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