]>
Commit | Line | Data |
---|---|---|
f9017ec1 | 1 | import dns |
22cf3506 | 2 | import json |
f9017ec1 | 3 | import os |
22cf3506 | 4 | import requests |
f9017ec1 RG |
5 | import socket |
6 | import struct | |
7 | import sys | |
8 | import threading | |
9 | import time | |
10 | ||
11 | from recursortests import RecursorTest | |
12 | ||
13 | class RPZServer(object): | |
14 | ||
15 | def __init__(self, port): | |
16 | self._currentSerial = 0 | |
17 | self._targetSerial = 1 | |
18 | self._serverPort = port | |
19 | listener = threading.Thread(name='RPZ Listener', target=self._listener, args=[]) | |
20 | listener.setDaemon(True) | |
21 | listener.start() | |
22 | ||
23 | def getCurrentSerial(self): | |
24 | return self._currentSerial | |
25 | ||
26 | def moveToSerial(self, newSerial): | |
27 | if newSerial == self._currentSerial: | |
28 | return False | |
29 | ||
30 | if newSerial != self._currentSerial + 1: | |
31 | raise AssertionError("Asking the RPZ server to server serial %d, already serving %d" % (newSerial, self._currentSerial)) | |
32 | self._targetSerial = newSerial | |
33 | return True | |
34 | ||
35 | def _getAnswer(self, message): | |
36 | ||
37 | response = dns.message.make_response(message) | |
38 | records = [] | |
39 | ||
40 | if message.question[0].rdtype == dns.rdatatype.AXFR: | |
41 | if self._currentSerial != 0: | |
42 | print('Received an AXFR query but IXFR expected because the current serial is %d' % (self._currentSerial)) | |
43 | return (None, self._currentSerial) | |
44 | ||
45 | newSerial = self._targetSerial | |
46 | records = [ | |
47 | dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial), | |
48 | dns.rrset.from_text('a.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1'), | |
49 | dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial) | |
50 | ] | |
51 | ||
52 | elif message.question[0].rdtype == dns.rdatatype.IXFR: | |
53 | oldSerial = message.authority[0][0].serial | |
54 | ||
55 | if oldSerial != self._currentSerial: | |
56 | print('Received an IXFR query with an unexpected serial %d, expected %d' % (oldSerial, self._currentSerial)) | |
57 | return (None, self._currentSerial) | |
58 | ||
59 | newSerial = self._targetSerial | |
60 | if newSerial == 2: | |
61 | records = [ | |
62 | 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), | |
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' % oldSerial), | |
64 | # no deletion | |
65 | 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), | |
66 | dns.rrset.from_text('b.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1'), | |
67 | 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) | |
68 | ] | |
69 | elif newSerial == 3: | |
70 | records = [ | |
71 | 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), | |
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' % oldSerial), | |
73 | dns.rrset.from_text('a.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1'), | |
74 | 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), | |
75 | # no addition | |
76 | 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) | |
77 | ] | |
78 | elif newSerial == 4: | |
79 | records = [ | |
80 | 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), | |
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' % oldSerial), | |
82 | dns.rrset.from_text('b.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1'), | |
83 | 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), | |
84 | dns.rrset.from_text('c.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1'), | |
85 | 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) | |
86 | ] | |
22cf3506 RG |
87 | elif newSerial == 5: |
88 | # this one is a bit special, we are answering with a full AXFR | |
89 | records = [ | |
90 | 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), | |
91 | dns.rrset.from_text('d.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1'), | |
8340237f RG |
92 | dns.rrset.from_text('tc.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.CNAME, 'rpz-tcp-only.'), |
93 | dns.rrset.from_text('drop.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.CNAME, 'rpz-drop.'), | |
22cf3506 RG |
94 | 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) |
95 | ] | |
96 | elif newSerial == 6: | |
97 | # back to IXFR | |
98 | records = [ | |
99 | 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), | |
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' % oldSerial), | |
101 | dns.rrset.from_text('d.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1'), | |
8340237f RG |
102 | dns.rrset.from_text('tc.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.CNAME, 'rpz-tcp-only.'), |
103 | dns.rrset.from_text('drop.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.CNAME, 'rpz-drop.'), | |
22cf3506 | 104 | 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 |
105 | dns.rrset.from_text('e.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1', '192.0.2.2'), |
106 | dns.rrset.from_text('e.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.MX, '10 mx.example.'), | |
107 | dns.rrset.from_text('f.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.CNAME, 'e.example.'), | |
108 | 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) | |
109 | ] | |
110 | elif newSerial == 7: | |
111 | records = [ | |
112 | 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), | |
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' % oldSerial), | |
114 | dns.rrset.from_text('e.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1', '192.0.2.2'), | |
115 | 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), | |
116 | dns.rrset.from_text('e.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.2'), | |
d13c4d18 RG |
117 | dns.rrset.from_text('tc.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.CNAME, 'rpz-tcp-only.'), |
118 | dns.rrset.from_text('drop.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.CNAME, 'rpz-drop.'), | |
22cf3506 RG |
119 | 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) |
120 | ] | |
98b33176 RG |
121 | elif newSerial == 8: |
122 | # this one is a bit special too, we are answering with a full AXFR and the new zone is empty | |
123 | records = [ | |
124 | 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), | |
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 | ] | |
f9017ec1 RG |
127 | |
128 | response.answer = records | |
129 | return (newSerial, response) | |
130 | ||
131 | def _connectionHandler(self, conn): | |
132 | data = None | |
133 | while True: | |
134 | data = conn.recv(2) | |
135 | if not data: | |
136 | break | |
137 | (datalen,) = struct.unpack("!H", data) | |
138 | data = conn.recv(datalen) | |
139 | if not data: | |
140 | break | |
141 | ||
142 | message = dns.message.from_wire(data) | |
143 | if len(message.question) != 1: | |
144 | print('Invalid RPZ query, qdcount is %d' % (len(message.question))) | |
145 | break | |
146 | if not message.question[0].rdtype in [dns.rdatatype.AXFR, dns.rdatatype.IXFR]: | |
147 | print('Invalid RPZ query, qtype is %d' % (message.question.rdtype)) | |
148 | break | |
149 | (serial, answer) = self._getAnswer(message) | |
150 | if not answer: | |
151 | print('Unable to get a response for %s %d' % (message.question[0].name, message.question[0].rdtype)) | |
152 | break | |
153 | ||
154 | wire = answer.to_wire() | |
155 | conn.send(struct.pack("!H", len(wire))) | |
156 | conn.send(wire) | |
157 | self._currentSerial = serial | |
158 | break | |
159 | ||
160 | conn.close() | |
161 | ||
162 | def _listener(self): | |
163 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |
164 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) | |
165 | try: | |
166 | sock.bind(("127.0.0.1", self._serverPort)) | |
167 | except socket.error as e: | |
168 | print("Error binding in the RPZ listener: %s" % str(e)) | |
169 | sys.exit(1) | |
170 | ||
171 | sock.listen(100) | |
172 | while True: | |
173 | try: | |
174 | (conn, _) = sock.accept() | |
175 | thread = threading.Thread(name='RPZ Connection Handler', | |
176 | target=self._connectionHandler, | |
177 | args=[conn]) | |
178 | thread.setDaemon(True) | |
179 | thread.start() | |
180 | ||
181 | except socket.error as e: | |
182 | print('Error in RPZ socket: %s' % str(e)) | |
183 | sock.close() | |
184 | ||
f9017ec1 | 185 | class RPZRecursorTest(RecursorTest): |
22cf3506 RG |
186 | _wsPort = 8042 |
187 | _wsTimeout = 2 | |
188 | _wsPassword = 'secretpassword' | |
189 | _apiKey = 'secretapikey' | |
f9017ec1 | 190 | _confdir = 'RPZ' |
d19bcbf0 RG |
191 | _lua_dns_script_file = """ |
192 | ||
193 | function prerpz(dq) | |
194 | -- disable the RPZ policy named 'zone.rpz' for AD=1 queries | |
195 | if dq:getDH():getAD() then | |
196 | dq:discardPolicy('zone.rpz.') | |
197 | end | |
198 | return false | |
199 | end | |
200 | """ | |
201 | ||
f9017ec1 | 202 | _config_template = """ |
22cf3506 RG |
203 | auth-zones=example=configs/%s/example.zone |
204 | webserver=yes | |
205 | webserver-port=%d | |
206 | webserver-address=127.0.0.1 | |
207 | webserver-password=%s | |
208 | api-key=%s | |
98b33176 | 209 | log-rpz-changes=yes |
22cf3506 | 210 | """ % (_confdir, _wsPort, _wsPassword, _apiKey) |
f9017ec1 RG |
211 | |
212 | @classmethod | |
213 | def setUpClass(cls): | |
214 | ||
215 | cls.setUpSockets() | |
216 | cls.startResponders() | |
217 | ||
218 | confdir = os.path.join('configs', cls._confdir) | |
219 | cls.createConfigDir(confdir) | |
220 | ||
221 | cls.generateRecursorConfig(confdir) | |
222 | cls.startRecursor(confdir, cls._recursorPort) | |
223 | ||
224 | @classmethod | |
225 | def tearDownClass(cls): | |
226 | cls.tearDownRecursor() | |
227 | ||
d19bcbf0 | 228 | def checkBlocked(self, name, shouldBeBlocked=True, adQuery=False): |
f9017ec1 RG |
229 | query = dns.message.make_query(name, 'A', want_dnssec=True) |
230 | query.flags |= dns.flags.CD | |
d19bcbf0 RG |
231 | if adQuery: |
232 | query.flags |= dns.flags.AD | |
f9017ec1 | 233 | |
d13c4d18 RG |
234 | for method in ("sendUDPQuery", "sendTCPQuery"): |
235 | sender = getattr(self, method) | |
236 | res = sender(query) | |
237 | self.assertRcodeEqual(res, dns.rcode.NOERROR) | |
238 | if shouldBeBlocked: | |
239 | expected = dns.rrset.from_text(name, 0, dns.rdataclass.IN, 'A', '192.0.2.1') | |
240 | else: | |
241 | expected = dns.rrset.from_text(name, 0, dns.rdataclass.IN, 'A', '192.0.2.42') | |
242 | ||
243 | self.assertRRsetInAnswer(res, expected) | |
f9017ec1 | 244 | |
d19bcbf0 RG |
245 | def checkNotBlocked(self, name, adQuery=False): |
246 | self.checkBlocked(name, False, adQuery) | |
f9017ec1 | 247 | |
6da513b2 RG |
248 | def checkCustom(self, qname, qtype, expected): |
249 | query = dns.message.make_query(qname, qtype, want_dnssec=True) | |
250 | query.flags |= dns.flags.CD | |
d13c4d18 RG |
251 | for method in ("sendUDPQuery", "sendTCPQuery"): |
252 | sender = getattr(self, method) | |
253 | res = sender(query) | |
254 | self.assertRcodeEqual(res, dns.rcode.NOERROR) | |
255 | self.assertRRsetInAnswer(res, expected) | |
6da513b2 RG |
256 | |
257 | def checkNoData(self, qname, qtype): | |
d13c4d18 RG |
258 | query = dns.message.make_query(qname, qtype, want_dnssec=True) |
259 | query.flags |= dns.flags.CD | |
260 | for method in ("sendUDPQuery", "sendTCPQuery"): | |
261 | sender = getattr(self, method) | |
262 | res = sender(query) | |
263 | self.assertRcodeEqual(res, dns.rcode.NOERROR) | |
264 | self.assertEqual(len(res.answer), 0) | |
265 | ||
98b33176 | 266 | def checkNXD(self, qname, qtype='A'): |
d122dac0 RG |
267 | query = dns.message.make_query(qname, qtype, want_dnssec=True) |
268 | query.flags |= dns.flags.CD | |
269 | for method in ("sendUDPQuery", "sendTCPQuery"): | |
270 | sender = getattr(self, method) | |
271 | res = sender(query) | |
272 | self.assertRcodeEqual(res, dns.rcode.NXDOMAIN) | |
273 | self.assertEqual(len(res.answer), 0) | |
274 | self.assertEqual(len(res.authority), 1) | |
275 | ||
d13c4d18 | 276 | def checkTruncated(self, qname, qtype='A'): |
6da513b2 RG |
277 | query = dns.message.make_query(qname, qtype, want_dnssec=True) |
278 | query.flags |= dns.flags.CD | |
279 | res = self.sendUDPQuery(query) | |
d13c4d18 RG |
280 | self.assertRcodeEqual(res, dns.rcode.NOERROR) |
281 | self.assertMessageHasFlags(res, ['QR', 'RA', 'RD', 'CD', 'TC']) | |
282 | self.assertEqual(len(res.answer), 0) | |
283 | self.assertEqual(len(res.authority), 0) | |
284 | self.assertEqual(len(res.additional), 0) | |
6da513b2 | 285 | |
d13c4d18 RG |
286 | res = self.sendTCPQuery(query) |
287 | self.assertRcodeEqual(res, dns.rcode.NXDOMAIN) | |
288 | self.assertMessageHasFlags(res, ['QR', 'RA', 'RD', 'CD']) | |
6da513b2 | 289 | self.assertEqual(len(res.answer), 0) |
d13c4d18 RG |
290 | self.assertEqual(len(res.authority), 1) |
291 | self.assertEqual(len(res.additional), 0) | |
292 | ||
293 | def checkDropped(self, qname, qtype='A'): | |
294 | query = dns.message.make_query(qname, qtype, want_dnssec=True) | |
295 | query.flags |= dns.flags.CD | |
296 | for method in ("sendUDPQuery", "sendTCPQuery"): | |
297 | sender = getattr(self, method) | |
298 | res = sender(query) | |
299 | self.assertEqual(res, None) | |
6da513b2 | 300 | |
d122dac0 RG |
301 | def checkRPZStats(self, serial, recordsCount, fullXFRCount, totalXFRCount): |
302 | headers = {'x-api-key': self._apiKey} | |
303 | url = 'http://127.0.0.1:' + str(self._wsPort) + '/api/v1/servers/localhost/rpzstatistics' | |
304 | r = requests.get(url, headers=headers, timeout=self._wsTimeout) | |
305 | self.assertTrue(r) | |
306 | self.assertEquals(r.status_code, 200) | |
307 | self.assertTrue(r.json()) | |
308 | content = r.json() | |
309 | self.assertIn('zone.rpz.', content) | |
310 | zone = content['zone.rpz.'] | |
311 | for key in ['last_update', 'records', 'serial', 'transfers_failed', 'transfers_full', 'transfers_success']: | |
312 | self.assertIn(key, zone) | |
313 | ||
314 | self.assertEquals(zone['serial'], serial) | |
315 | self.assertEquals(zone['records'], recordsCount) | |
316 | self.assertEquals(zone['transfers_full'], fullXFRCount) | |
317 | self.assertEquals(zone['transfers_success'], totalXFRCount) | |
318 | ||
319 | rpzServerPort = 4250 | |
320 | rpzServer = RPZServer(rpzServerPort) | |
321 | ||
322 | class RPZXFRRecursorTest(RPZRecursorTest): | |
323 | """ | |
324 | This test makes sure that we correctly update RPZ zones via AXFR then IXFR | |
325 | """ | |
326 | ||
327 | global rpzServerPort | |
328 | _lua_config_file = """ | |
329 | -- The first server is a bogus one, to test that we correctly fail over to the second one | |
330 | rpzMaster({'127.0.0.1:9999', '127.0.0.1:%d'}, 'zone.rpz.', { refresh=1 }) | |
331 | """ % (rpzServerPort) | |
332 | _confdir = 'RPZXFR' | |
333 | _wsPort = 8042 | |
334 | _wsTimeout = 2 | |
335 | _wsPassword = 'secretpassword' | |
336 | _apiKey = 'secretapikey' | |
337 | _config_template = """ | |
338 | auth-zones=example=configs/%s/example.zone | |
339 | webserver=yes | |
340 | webserver-port=%d | |
341 | webserver-address=127.0.0.1 | |
342 | webserver-password=%s | |
343 | api-key=%s | |
344 | """ % (_confdir, _wsPort, _wsPassword, _apiKey) | |
345 | _xfrDone = 0 | |
346 | ||
347 | @classmethod | |
348 | def generateRecursorConfig(cls, confdir): | |
349 | authzonepath = os.path.join(confdir, 'example.zone') | |
350 | with open(authzonepath, 'w') as authzone: | |
351 | authzone.write("""$ORIGIN example. | |
352 | @ 3600 IN SOA {soa} | |
353 | a 3600 IN A 192.0.2.42 | |
354 | b 3600 IN A 192.0.2.42 | |
355 | c 3600 IN A 192.0.2.42 | |
356 | d 3600 IN A 192.0.2.42 | |
357 | e 3600 IN A 192.0.2.42 | |
358 | """.format(soa=cls._SOA)) | |
359 | super(RPZRecursorTest, cls).generateRecursorConfig(confdir) | |
360 | ||
f9017ec1 RG |
361 | def waitUntilCorrectSerialIsLoaded(self, serial, timeout=5): |
362 | global rpzServer | |
363 | ||
364 | rpzServer.moveToSerial(serial) | |
365 | ||
366 | attempts = 0 | |
367 | while attempts < timeout: | |
368 | currentSerial = rpzServer.getCurrentSerial() | |
369 | if currentSerial > serial: | |
370 | raise AssertionError("Expected serial %d, got %d" % (serial, currentSerial)) | |
371 | if currentSerial == serial: | |
22cf3506 | 372 | self._xfrDone = self._xfrDone + 1 |
f9017ec1 RG |
373 | return |
374 | ||
375 | attempts = attempts + 1 | |
376 | time.sleep(1) | |
377 | ||
378 | raise AssertionError("Waited %d seconds for the serial to be updated to %d but the serial is still %d" % (timeout, serial, currentSerial)) | |
379 | ||
380 | def testRPZ(self): | |
381 | # first zone, only a should be blocked | |
382 | self.waitUntilCorrectSerialIsLoaded(1) | |
22cf3506 | 383 | self.checkRPZStats(1, 1, 1, self._xfrDone) |
f9017ec1 RG |
384 | self.checkBlocked('a.example.') |
385 | self.checkNotBlocked('b.example.') | |
386 | self.checkNotBlocked('c.example.') | |
387 | ||
388 | # second zone, a and b should be blocked | |
389 | self.waitUntilCorrectSerialIsLoaded(2) | |
22cf3506 | 390 | self.checkRPZStats(2, 2, 1, self._xfrDone) |
f9017ec1 RG |
391 | self.checkBlocked('a.example.') |
392 | self.checkBlocked('b.example.') | |
393 | self.checkNotBlocked('c.example.') | |
394 | ||
395 | # third zone, only b should be blocked | |
396 | self.waitUntilCorrectSerialIsLoaded(3) | |
22cf3506 | 397 | self.checkRPZStats(3, 1, 1, self._xfrDone) |
f9017ec1 RG |
398 | self.checkNotBlocked('a.example.') |
399 | self.checkBlocked('b.example.') | |
400 | self.checkNotBlocked('c.example.') | |
401 | ||
402 | # fourth zone, only c should be blocked | |
403 | self.waitUntilCorrectSerialIsLoaded(4) | |
22cf3506 | 404 | self.checkRPZStats(4, 1, 1, self._xfrDone) |
f9017ec1 RG |
405 | self.checkNotBlocked('a.example.') |
406 | self.checkNotBlocked('b.example.') | |
407 | self.checkBlocked('c.example.') | |
22cf3506 RG |
408 | |
409 | # fifth zone, we should get a full AXFR this time, and only d should be blocked | |
410 | self.waitUntilCorrectSerialIsLoaded(5) | |
8340237f | 411 | self.checkRPZStats(5, 3, 2, self._xfrDone) |
22cf3506 RG |
412 | self.checkNotBlocked('a.example.') |
413 | self.checkNotBlocked('b.example.') | |
414 | self.checkNotBlocked('c.example.') | |
415 | self.checkBlocked('d.example.') | |
416 | ||
6da513b2 | 417 | # sixth zone, only e should be blocked, f is a local data record |
22cf3506 | 418 | self.waitUntilCorrectSerialIsLoaded(6) |
6da513b2 RG |
419 | self.checkRPZStats(6, 2, 2, self._xfrDone) |
420 | self.checkNotBlocked('a.example.') | |
421 | self.checkNotBlocked('b.example.') | |
422 | self.checkNotBlocked('c.example.') | |
423 | self.checkNotBlocked('d.example.') | |
424 | self.checkCustom('e.example.', 'A', dns.rrset.from_text('e.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.1', '192.0.2.2')) | |
425 | self.checkCustom('e.example.', 'MX', dns.rrset.from_text('e.example.', 0, dns.rdataclass.IN, 'MX', '10 mx.example.')) | |
426 | self.checkNoData('e.example.', 'AAAA') | |
427 | self.checkCustom('f.example.', 'A', dns.rrset.from_text('f.example.', 0, dns.rdataclass.IN, 'CNAME', 'e.example.')) | |
428 | ||
429 | # seventh zone, e should only have one A | |
430 | self.waitUntilCorrectSerialIsLoaded(7) | |
d13c4d18 | 431 | self.checkRPZStats(7, 4, 2, self._xfrDone) |
22cf3506 RG |
432 | self.checkNotBlocked('a.example.') |
433 | self.checkNotBlocked('b.example.') | |
434 | self.checkNotBlocked('c.example.') | |
435 | self.checkNotBlocked('d.example.') | |
6da513b2 RG |
436 | self.checkCustom('e.example.', 'A', dns.rrset.from_text('e.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.2')) |
437 | self.checkCustom('e.example.', 'MX', dns.rrset.from_text('e.example.', 0, dns.rdataclass.IN, 'MX', '10 mx.example.')) | |
438 | self.checkNoData('e.example.', 'AAAA') | |
439 | self.checkCustom('f.example.', 'A', dns.rrset.from_text('f.example.', 0, dns.rdataclass.IN, 'CNAME', 'e.example.')) | |
d19bcbf0 RG |
440 | # check that the policy is disabled for AD=1 queries |
441 | self.checkNotBlocked('e.example.', True) | |
d13c4d18 RG |
442 | # check non-custom policies |
443 | self.checkTruncated('tc.example.') | |
444 | self.checkDropped('drop.example.') | |
d122dac0 | 445 | |
98b33176 RG |
446 | # eighth zone, all entries should be gone |
447 | self.waitUntilCorrectSerialIsLoaded(8) | |
448 | self.checkRPZStats(8, 0, 3, self._xfrDone) | |
449 | self.checkNotBlocked('a.example.') | |
450 | self.checkNotBlocked('b.example.') | |
451 | self.checkNotBlocked('c.example.') | |
452 | self.checkNotBlocked('d.example.') | |
453 | self.checkNotBlocked('e.example.') | |
454 | self.checkNXD('f.example.') | |
455 | self.checkNXD('tc.example.') | |
456 | self.checkNXD('drop.example.') | |
457 | ||
d122dac0 RG |
458 | class RPZFileRecursorTest(RPZRecursorTest): |
459 | """ | |
460 | This test makes sure that we correctly load RPZ zones from a file | |
461 | """ | |
462 | ||
463 | _confdir = 'RPZFile' | |
464 | _wsPort = 8042 | |
465 | _wsTimeout = 2 | |
466 | _wsPassword = 'secretpassword' | |
467 | _apiKey = 'secretapikey' | |
468 | _lua_config_file = """ | |
469 | rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz." }) | |
470 | """ % (_confdir) | |
471 | _config_template = """ | |
472 | auth-zones=example=configs/%s/example.zone | |
473 | webserver=yes | |
474 | webserver-port=%d | |
475 | webserver-address=127.0.0.1 | |
476 | webserver-password=%s | |
477 | api-key=%s | |
478 | """ % (_confdir, _wsPort, _wsPassword, _apiKey) | |
479 | ||
480 | @classmethod | |
481 | def generateRecursorConfig(cls, confdir): | |
482 | authzonepath = os.path.join(confdir, 'example.zone') | |
483 | with open(authzonepath, 'w') as authzone: | |
484 | authzone.write("""$ORIGIN example. | |
485 | @ 3600 IN SOA {soa} | |
486 | a 3600 IN A 192.0.2.42 | |
487 | b 3600 IN A 192.0.2.42 | |
488 | c 3600 IN A 192.0.2.42 | |
489 | d 3600 IN A 192.0.2.42 | |
490 | e 3600 IN A 192.0.2.42 | |
491 | z 3600 IN A 192.0.2.42 | |
492 | """.format(soa=cls._SOA)) | |
493 | ||
494 | rpzFilePath = os.path.join(confdir, 'zone.rpz') | |
495 | with open(rpzFilePath, 'w') as rpzZone: | |
496 | rpzZone.write("""$ORIGIN zone.rpz. | |
497 | @ 3600 IN SOA {soa} | |
498 | a.example.zone.rpz. 60 IN A 192.0.2.42 | |
499 | a.example.zone.rpz. 60 IN A 192.0.2.43 | |
500 | a.example.zone.rpz. 60 IN TXT "some text" | |
501 | drop.example.zone.rpz. 60 IN CNAME rpz-drop. | |
502 | z.example.zone.rpz. 60 IN A 192.0.2.1 | |
503 | tc.example.zone.rpz. 60 IN CNAME rpz-tcp-only. | |
504 | """.format(soa=cls._SOA)) | |
505 | super(RPZFileRecursorTest, cls).generateRecursorConfig(confdir) | |
506 | ||
507 | def testRPZ(self): | |
508 | self.checkCustom('a.example.', 'A', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.42', '192.0.2.43')) | |
509 | self.checkCustom('a.example.', 'TXT', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'TXT', '"some text"')) | |
510 | self.checkBlocked('z.example.') | |
511 | self.checkNotBlocked('b.example.') | |
512 | self.checkNotBlocked('c.example.') | |
513 | self.checkNotBlocked('d.example.') | |
514 | self.checkNotBlocked('e.example.') | |
515 | # check that the policy is disabled for AD=1 queries | |
516 | self.checkNotBlocked('z.example.', True) | |
517 | # check non-custom policies | |
518 | self.checkTruncated('tc.example.') | |
519 | self.checkDropped('drop.example.') | |
520 | ||
521 | class RPZFileDefaultPolRecursorTest(RPZRecursorTest): | |
522 | """ | |
523 | This test makes sure that we correctly load RPZ zones from a file with a default policy | |
524 | """ | |
525 | ||
526 | _confdir = 'RPZFileDefaultPolicy' | |
527 | _wsPort = 8042 | |
528 | _wsTimeout = 2 | |
529 | _wsPassword = 'secretpassword' | |
530 | _apiKey = 'secretapikey' | |
531 | _lua_config_file = """ | |
532 | rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz.", defpol=Policy.NoAction }) | |
533 | """ % (_confdir) | |
534 | _config_template = """ | |
535 | auth-zones=example=configs/%s/example.zone | |
536 | webserver=yes | |
537 | webserver-port=%d | |
538 | webserver-address=127.0.0.1 | |
539 | webserver-password=%s | |
540 | api-key=%s | |
541 | """ % (_confdir, _wsPort, _wsPassword, _apiKey) | |
542 | ||
543 | @classmethod | |
544 | def generateRecursorConfig(cls, confdir): | |
545 | authzonepath = os.path.join(confdir, 'example.zone') | |
546 | with open(authzonepath, 'w') as authzone: | |
547 | authzone.write("""$ORIGIN example. | |
548 | @ 3600 IN SOA {soa} | |
549 | a 3600 IN A 192.0.2.42 | |
550 | b 3600 IN A 192.0.2.42 | |
551 | c 3600 IN A 192.0.2.42 | |
552 | d 3600 IN A 192.0.2.42 | |
553 | drop 3600 IN A 192.0.2.42 | |
554 | e 3600 IN A 192.0.2.42 | |
555 | z 3600 IN A 192.0.2.42 | |
556 | """.format(soa=cls._SOA)) | |
557 | ||
558 | rpzFilePath = os.path.join(confdir, 'zone.rpz') | |
559 | with open(rpzFilePath, 'w') as rpzZone: | |
560 | rpzZone.write("""$ORIGIN zone.rpz. | |
561 | @ 3600 IN SOA {soa} | |
562 | a.example.zone.rpz. 60 IN A 192.0.2.42 | |
563 | drop.example.zone.rpz. 60 IN CNAME rpz-drop. | |
564 | z.example.zone.rpz. 60 IN A 192.0.2.1 | |
565 | tc.example.zone.rpz. 60 IN CNAME rpz-tcp-only. | |
566 | """.format(soa=cls._SOA)) | |
567 | super(RPZFileDefaultPolRecursorTest, cls).generateRecursorConfig(confdir) | |
568 | ||
569 | def testRPZ(self): | |
570 | # local data entries are overridden by default | |
571 | self.checkCustom('a.example.', 'A', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.42')) | |
572 | self.checkNoData('a.example.', 'TXT') | |
573 | # will not be blocked because the default policy overrides local data entries by default | |
574 | self.checkNotBlocked('z.example.') | |
575 | self.checkNotBlocked('b.example.') | |
576 | self.checkNotBlocked('c.example.') | |
577 | self.checkNotBlocked('d.example.') | |
578 | self.checkNotBlocked('e.example.') | |
579 | # check non-local policies, they should be overridden by the default policy | |
580 | self.checkNXD('tc.example.', 'A') | |
581 | self.checkNotBlocked('drop.example.') | |
582 | ||
583 | class RPZFileDefaultPolNotOverrideLocalRecursorTest(RPZRecursorTest): | |
584 | """ | |
585 | This test makes sure that we correctly load RPZ zones from a file with a default policy, not overriding local data entries | |
586 | """ | |
587 | ||
588 | _confdir = 'RPZFileDefaultPolicyNotOverrideLocal' | |
589 | _wsPort = 8042 | |
590 | _wsTimeout = 2 | |
591 | _wsPassword = 'secretpassword' | |
592 | _apiKey = 'secretapikey' | |
593 | _lua_config_file = """ | |
594 | rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz.", defpol=Policy.NoAction, defpolOverrideLocalData=false }) | |
595 | """ % (_confdir) | |
596 | _config_template = """ | |
597 | auth-zones=example=configs/%s/example.zone | |
598 | webserver=yes | |
599 | webserver-port=%d | |
600 | webserver-address=127.0.0.1 | |
601 | webserver-password=%s | |
602 | api-key=%s | |
603 | """ % (_confdir, _wsPort, _wsPassword, _apiKey) | |
604 | ||
605 | @classmethod | |
606 | def generateRecursorConfig(cls, confdir): | |
607 | authzonepath = os.path.join(confdir, 'example.zone') | |
608 | with open(authzonepath, 'w') as authzone: | |
609 | authzone.write("""$ORIGIN example. | |
610 | @ 3600 IN SOA {soa} | |
611 | a 3600 IN A 192.0.2.42 | |
612 | b 3600 IN A 192.0.2.42 | |
613 | c 3600 IN A 192.0.2.42 | |
614 | d 3600 IN A 192.0.2.42 | |
615 | drop 3600 IN A 192.0.2.42 | |
616 | e 3600 IN A 192.0.2.42 | |
617 | z 3600 IN A 192.0.2.42 | |
618 | """.format(soa=cls._SOA)) | |
619 | ||
620 | rpzFilePath = os.path.join(confdir, 'zone.rpz') | |
621 | with open(rpzFilePath, 'w') as rpzZone: | |
622 | rpzZone.write("""$ORIGIN zone.rpz. | |
623 | @ 3600 IN SOA {soa} | |
624 | a.example.zone.rpz. 60 IN A 192.0.2.42 | |
625 | a.example.zone.rpz. 60 IN A 192.0.2.43 | |
626 | a.example.zone.rpz. 60 IN TXT "some text" | |
627 | drop.example.zone.rpz. 60 IN CNAME rpz-drop. | |
628 | z.example.zone.rpz. 60 IN A 192.0.2.1 | |
629 | tc.example.zone.rpz. 60 IN CNAME rpz-tcp-only. | |
630 | """.format(soa=cls._SOA)) | |
631 | super(RPZFileDefaultPolNotOverrideLocalRecursorTest, cls).generateRecursorConfig(confdir) | |
632 | ||
633 | def testRPZ(self): | |
634 | # local data entries will not be overridden by the default polic | |
635 | self.checkCustom('a.example.', 'A', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.42', '192.0.2.43')) | |
636 | self.checkCustom('a.example.', 'TXT', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'TXT', '"some text"')) | |
637 | # will be blocked because the default policy does not override local data entries | |
638 | self.checkBlocked('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.') |