]>
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=[]) | |
046b69a5 | 20 | listener.daemon = True |
f9017ec1 RG |
21 | listener.start() |
22 | ||
23 | def getCurrentSerial(self): | |
24 | return self._currentSerial | |
25 | ||
26 | def moveToSerial(self, newSerial): | |
27 | if newSerial == self._currentSerial: | |
28 | return False | |
29 | ||
30 | if newSerial != self._currentSerial + 1: | |
ba5f46ae | 31 | raise AssertionError("Asking the RPZ server to serve serial %d, already serving %d" % (newSerial, self._currentSerial)) |
f9017ec1 RG |
32 | self._targetSerial = newSerial |
33 | return True | |
34 | ||
35 | def _getAnswer(self, message): | |
36 | ||
37 | response = dns.message.make_response(message) | |
38 | records = [] | |
39 | ||
40 | if message.question[0].rdtype == dns.rdatatype.AXFR: | |
41 | if self._currentSerial != 0: | |
42 | print('Received an AXFR query but IXFR expected because the current serial is %d' % (self._currentSerial)) | |
43 | return (None, self._currentSerial) | |
44 | ||
45 | newSerial = self._targetSerial | |
46 | records = [ | |
47 | dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial), | |
48 | dns.rrset.from_text('a.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1'), | |
49 | dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial) | |
50 | ] | |
51 | ||
52 | elif message.question[0].rdtype == dns.rdatatype.IXFR: | |
53 | oldSerial = message.authority[0][0].serial | |
54 | ||
ee2a5356 RG |
55 | # special case for the 9th update, which might get skipped |
56 | if oldSerial != self._currentSerial and self._currentSerial != 9: | |
f9017ec1 RG |
57 | print('Received an IXFR query with an unexpected serial %d, expected %d' % (oldSerial, self._currentSerial)) |
58 | return (None, self._currentSerial) | |
59 | ||
60 | newSerial = self._targetSerial | |
61 | if newSerial == 2: | |
62 | records = [ | |
63 | dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial), | |
64 | dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % oldSerial), | |
65 | # no deletion | |
66 | dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial), | |
67 | dns.rrset.from_text('b.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1'), | |
68 | dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial) | |
69 | ] | |
70 | elif newSerial == 3: | |
71 | records = [ | |
72 | dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial), | |
73 | dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % oldSerial), | |
74 | dns.rrset.from_text('a.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1'), | |
75 | dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial), | |
76 | # no addition | |
77 | dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial) | |
78 | ] | |
79 | elif newSerial == 4: | |
80 | records = [ | |
81 | dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial), | |
82 | dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % oldSerial), | |
83 | dns.rrset.from_text('b.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1'), | |
84 | dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial), | |
85 | dns.rrset.from_text('c.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1'), | |
86 | dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial) | |
87 | ] | |
22cf3506 RG |
88 | elif newSerial == 5: |
89 | # this one is a bit special, we are answering with a full AXFR | |
90 | records = [ | |
91 | dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial), | |
92 | dns.rrset.from_text('d.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1'), | |
8340237f RG |
93 | dns.rrset.from_text('tc.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.CNAME, 'rpz-tcp-only.'), |
94 | dns.rrset.from_text('drop.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.CNAME, 'rpz-drop.'), | |
22cf3506 RG |
95 | dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial) |
96 | ] | |
97 | elif newSerial == 6: | |
98 | # back to IXFR | |
99 | records = [ | |
100 | dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial), | |
101 | dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % oldSerial), | |
102 | dns.rrset.from_text('d.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1'), | |
8340237f RG |
103 | dns.rrset.from_text('tc.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.CNAME, 'rpz-tcp-only.'), |
104 | dns.rrset.from_text('drop.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.CNAME, 'rpz-drop.'), | |
22cf3506 | 105 | dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial), |
6da513b2 RG |
106 | dns.rrset.from_text('e.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1', '192.0.2.2'), |
107 | dns.rrset.from_text('e.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.MX, '10 mx.example.'), | |
108 | dns.rrset.from_text('f.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.CNAME, 'e.example.'), | |
109 | dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial) | |
110 | ] | |
111 | elif newSerial == 7: | |
112 | records = [ | |
113 | dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial), | |
114 | dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % oldSerial), | |
115 | dns.rrset.from_text('e.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1', '192.0.2.2'), | |
116 | dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial), | |
117 | dns.rrset.from_text('e.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.2'), | |
d13c4d18 RG |
118 | dns.rrset.from_text('tc.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.CNAME, 'rpz-tcp-only.'), |
119 | dns.rrset.from_text('drop.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.CNAME, 'rpz-drop.'), | |
22cf3506 RG |
120 | dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial) |
121 | ] | |
98b33176 RG |
122 | elif newSerial == 8: |
123 | # this one is a bit special too, we are answering with a full AXFR and the new zone is empty | |
124 | records = [ | |
125 | dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial), | |
126 | dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial) | |
127 | ] | |
ee2a5356 RG |
128 | elif newSerial == 9: |
129 | # IXFR inserting a duplicate, we should not crash and skip it | |
130 | records = [ | |
131 | dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial), | |
132 | dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % oldSerial), | |
133 | dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial), | |
134 | dns.rrset.from_text('dup.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.CNAME, 'rpz-passthru.'), | |
135 | dns.rrset.from_text('dup.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.CNAME, 'rpz-passthru.'), | |
136 | dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial) | |
137 | ] | |
138 | elif newSerial == 10: | |
ba5f46ae | 139 | # full AXFR to make sure we are removing the duplicate, adding a record, to check that the update was correctly applied |
ee2a5356 RG |
140 | records = [ |
141 | dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial), | |
142 | dns.rrset.from_text('f.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1'), | |
143 | dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial) | |
144 | ] | |
ba5f46ae RG |
145 | elif newSerial == 11: |
146 | # IXFR with two deltas, the first one adding a 'g' and the second one removing 'f' | |
147 | records = [ | |
148 | dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % (newSerial + 1)), | |
149 | dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % oldSerial), | |
150 | dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial), | |
151 | dns.rrset.from_text('g.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1'), | |
152 | dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial), | |
153 | dns.rrset.from_text('f.example.zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.1'), | |
154 | dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % (newSerial + 1)), | |
155 | dns.rrset.from_text('zone.rpz.', 60, dns.rdataclass.IN, dns.rdatatype.SOA, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % (newSerial + 1)) | |
156 | ] | |
157 | # this one has two updates in one | |
158 | newSerial = newSerial + 1 | |
159 | self._targetSerial = self._targetSerial + 1 | |
f9017ec1 RG |
160 | |
161 | response.answer = records | |
162 | return (newSerial, response) | |
163 | ||
164 | def _connectionHandler(self, conn): | |
165 | data = None | |
166 | while True: | |
167 | data = conn.recv(2) | |
168 | if not data: | |
169 | break | |
170 | (datalen,) = struct.unpack("!H", data) | |
171 | data = conn.recv(datalen) | |
172 | if not data: | |
173 | break | |
174 | ||
175 | message = dns.message.from_wire(data) | |
176 | if len(message.question) != 1: | |
177 | print('Invalid RPZ query, qdcount is %d' % (len(message.question))) | |
178 | break | |
179 | if not message.question[0].rdtype in [dns.rdatatype.AXFR, dns.rdatatype.IXFR]: | |
180 | print('Invalid RPZ query, qtype is %d' % (message.question.rdtype)) | |
181 | break | |
182 | (serial, answer) = self._getAnswer(message) | |
183 | if not answer: | |
184 | print('Unable to get a response for %s %d' % (message.question[0].name, message.question[0].rdtype)) | |
185 | break | |
186 | ||
187 | wire = answer.to_wire() | |
8faf5a90 PD |
188 | lenprefix = struct.pack("!H", len(wire)) |
189 | ||
190 | for b in lenprefix: | |
191 | conn.send(bytes([b])) | |
192 | time.sleep(0.5) | |
193 | ||
f9017ec1 RG |
194 | conn.send(wire) |
195 | self._currentSerial = serial | |
196 | break | |
197 | ||
198 | conn.close() | |
199 | ||
200 | def _listener(self): | |
201 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | |
202 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) | |
203 | try: | |
204 | sock.bind(("127.0.0.1", self._serverPort)) | |
205 | except socket.error as e: | |
206 | print("Error binding in the RPZ listener: %s" % str(e)) | |
207 | sys.exit(1) | |
208 | ||
209 | sock.listen(100) | |
210 | while True: | |
211 | try: | |
212 | (conn, _) = sock.accept() | |
213 | thread = threading.Thread(name='RPZ Connection Handler', | |
214 | target=self._connectionHandler, | |
215 | args=[conn]) | |
046b69a5 | 216 | thread.daemon = True |
f9017ec1 RG |
217 | thread.start() |
218 | ||
219 | except socket.error as e: | |
220 | print('Error in RPZ socket: %s' % str(e)) | |
221 | sock.close() | |
222 | ||
f9017ec1 | 223 | class 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 |
247 | auth-zones=example=configs/%s/example.zone |
248 | webserver=yes | |
249 | webserver-port=%d | |
250 | webserver-address=127.0.0.1 | |
251 | webserver-password=%s | |
252 | api-key=%s | |
98b33176 | 253 | log-rpz-changes=yes |
22cf3506 | 254 | """ % (_confdir, _wsPort, _wsPassword, _apiKey) |
f9017ec1 | 255 | |
046b69a5 OM |
256 | def sendNotify(self): |
257 | notify = dns.message.make_query('zone.rpz', 'SOA', want_dnssec=False) | |
258 | notify.set_opcode(4) # notify | |
259 | res = self.sendUDPQuery(notify) | |
260 | self.assertRcodeEqual(res, dns.rcode.NOERROR) | |
261 | self.assertEqual(res.opcode(), 4) | |
262 | self.assertEqual(res.question[0].to_text(), 'zone.rpz. IN SOA') | |
263 | ||
f6a524be OM |
264 | def assertAdditionalHasSOA(self, msg): |
265 | if not isinstance(msg, dns.message.Message): | |
266 | raise TypeError("msg is not a dns.message.Message but a %s" % type(msg)) | |
267 | ||
268 | found = False | |
269 | for rrset in msg.additional: | |
270 | if rrset.rdtype == dns.rdatatype.SOA: | |
271 | found = True | |
272 | break | |
273 | ||
274 | if not found: | |
275 | raise AssertionError("No SOA record found in the authority section:\n%s" % msg.to_text()) | |
276 | ||
277 | def checkBlocked(self, name, shouldBeBlocked=True, adQuery=False, singleCheck=False, soa=False): | |
f9017ec1 RG |
278 | query = dns.message.make_query(name, 'A', want_dnssec=True) |
279 | query.flags |= dns.flags.CD | |
d19bcbf0 RG |
280 | if adQuery: |
281 | query.flags |= dns.flags.AD | |
f9017ec1 | 282 | |
d13c4d18 RG |
283 | for method in ("sendUDPQuery", "sendTCPQuery"): |
284 | sender = getattr(self, method) | |
285 | res = sender(query) | |
286 | self.assertRcodeEqual(res, dns.rcode.NOERROR) | |
287 | if shouldBeBlocked: | |
288 | expected = dns.rrset.from_text(name, 0, dns.rdataclass.IN, 'A', '192.0.2.1') | |
289 | else: | |
290 | expected = dns.rrset.from_text(name, 0, dns.rdataclass.IN, 'A', '192.0.2.42') | |
291 | ||
292 | self.assertRRsetInAnswer(res, expected) | |
f6a524be OM |
293 | if soa: |
294 | self.assertAdditionalHasSOA(res) | |
f89ae456 RG |
295 | if singleCheck: |
296 | break | |
f9017ec1 | 297 | |
f89ae456 RG |
298 | def checkNotBlocked(self, name, adQuery=False, singleCheck=False): |
299 | self.checkBlocked(name, False, adQuery, singleCheck) | |
f9017ec1 | 300 | |
f6a524be | 301 | def checkCustom(self, qname, qtype, expected, soa=False): |
6da513b2 RG |
302 | query = dns.message.make_query(qname, qtype, want_dnssec=True) |
303 | query.flags |= dns.flags.CD | |
d13c4d18 RG |
304 | for method in ("sendUDPQuery", "sendTCPQuery"): |
305 | sender = getattr(self, method) | |
306 | res = sender(query) | |
307 | self.assertRcodeEqual(res, dns.rcode.NOERROR) | |
308 | self.assertRRsetInAnswer(res, expected) | |
f6a524be OM |
309 | if soa: |
310 | self.assertAdditionalHasSOA(res) | |
6da513b2 | 311 | |
f6a524be | 312 | def checkNoData(self, qname, qtype, soa=False): |
d13c4d18 RG |
313 | query = dns.message.make_query(qname, qtype, want_dnssec=True) |
314 | query.flags |= dns.flags.CD | |
315 | for method in ("sendUDPQuery", "sendTCPQuery"): | |
316 | sender = getattr(self, method) | |
317 | res = sender(query) | |
318 | self.assertRcodeEqual(res, dns.rcode.NOERROR) | |
319 | self.assertEqual(len(res.answer), 0) | |
f6a524be OM |
320 | if soa: |
321 | self.assertAdditionalHasSOA(res) | |
d13c4d18 | 322 | |
98b33176 | 323 | def checkNXD(self, qname, qtype='A'): |
d122dac0 RG |
324 | query = dns.message.make_query(qname, qtype, want_dnssec=True) |
325 | query.flags |= dns.flags.CD | |
326 | for method in ("sendUDPQuery", "sendTCPQuery"): | |
327 | sender = getattr(self, method) | |
328 | res = sender(query) | |
329 | self.assertRcodeEqual(res, dns.rcode.NXDOMAIN) | |
330 | self.assertEqual(len(res.answer), 0) | |
331 | self.assertEqual(len(res.authority), 1) | |
332 | ||
f6a524be | 333 | def checkTruncated(self, qname, qtype='A', soa=False): |
6da513b2 RG |
334 | query = dns.message.make_query(qname, qtype, want_dnssec=True) |
335 | query.flags |= dns.flags.CD | |
336 | res = self.sendUDPQuery(query) | |
d13c4d18 RG |
337 | self.assertRcodeEqual(res, dns.rcode.NOERROR) |
338 | self.assertMessageHasFlags(res, ['QR', 'RA', 'RD', 'CD', 'TC']) | |
339 | self.assertEqual(len(res.answer), 0) | |
340 | self.assertEqual(len(res.authority), 0) | |
f6a524be OM |
341 | if soa: |
342 | self.assertAdditionalHasSOA(res) | |
6da513b2 | 343 | |
d13c4d18 RG |
344 | res = self.sendTCPQuery(query) |
345 | self.assertRcodeEqual(res, dns.rcode.NXDOMAIN) | |
346 | self.assertMessageHasFlags(res, ['QR', 'RA', 'RD', 'CD']) | |
6da513b2 | 347 | self.assertEqual(len(res.answer), 0) |
d13c4d18 RG |
348 | self.assertEqual(len(res.authority), 1) |
349 | self.assertEqual(len(res.additional), 0) | |
350 | ||
351 | def checkDropped(self, qname, qtype='A'): | |
352 | query = dns.message.make_query(qname, qtype, want_dnssec=True) | |
353 | query.flags |= dns.flags.CD | |
354 | for method in ("sendUDPQuery", "sendTCPQuery"): | |
355 | sender = getattr(self, method) | |
356 | res = sender(query) | |
357 | self.assertEqual(res, None) | |
6da513b2 | 358 | |
d122dac0 RG |
359 | def checkRPZStats(self, serial, recordsCount, fullXFRCount, totalXFRCount): |
360 | headers = {'x-api-key': self._apiKey} | |
361 | url = 'http://127.0.0.1:' + str(self._wsPort) + '/api/v1/servers/localhost/rpzstatistics' | |
362 | r = requests.get(url, headers=headers, timeout=self._wsTimeout) | |
363 | self.assertTrue(r) | |
4bfebc93 | 364 | self.assertEqual(r.status_code, 200) |
d122dac0 RG |
365 | self.assertTrue(r.json()) |
366 | content = r.json() | |
367 | self.assertIn('zone.rpz.', content) | |
368 | zone = content['zone.rpz.'] | |
369 | for key in ['last_update', 'records', 'serial', 'transfers_failed', 'transfers_full', 'transfers_success']: | |
370 | self.assertIn(key, zone) | |
371 | ||
4bfebc93 CH |
372 | self.assertEqual(zone['serial'], serial) |
373 | self.assertEqual(zone['records'], recordsCount) | |
374 | self.assertEqual(zone['transfers_full'], fullXFRCount) | |
375 | self.assertEqual(zone['transfers_success'], totalXFRCount) | |
d122dac0 RG |
376 | |
377 | rpzServerPort = 4250 | |
378 | rpzServer = RPZServer(rpzServerPort) | |
379 | ||
380 | class RPZXFRRecursorTest(RPZRecursorTest): | |
381 | """ | |
382 | This test makes sure that we correctly update RPZ zones via AXFR then IXFR | |
383 | """ | |
384 | ||
385 | global rpzServerPort | |
386 | _lua_config_file = """ | |
387 | -- The first server is a bogus one, to test that we correctly fail over to the second one | |
71ada8e0 | 388 | rpzMaster({'127.0.0.1:9999', '127.0.0.1:%d'}, 'zone.rpz.', { refresh=1, includeSOA=true}) |
d122dac0 RG |
389 | """ % (rpzServerPort) |
390 | _confdir = 'RPZXFR' | |
391 | _wsPort = 8042 | |
392 | _wsTimeout = 2 | |
393 | _wsPassword = 'secretpassword' | |
394 | _apiKey = 'secretapikey' | |
395 | _config_template = """ | |
396 | auth-zones=example=configs/%s/example.zone | |
397 | webserver=yes | |
398 | webserver-port=%d | |
399 | webserver-address=127.0.0.1 | |
400 | webserver-password=%s | |
401 | api-key=%s | |
d2c1660a | 402 | disable-packetcache |
046b69a5 OM |
403 | allow-notify-from=127.0.0.0/8 |
404 | allow-notify-for=zone.rpz | |
d122dac0 RG |
405 | """ % (_confdir, _wsPort, _wsPassword, _apiKey) |
406 | _xfrDone = 0 | |
407 | ||
408 | @classmethod | |
409 | def generateRecursorConfig(cls, confdir): | |
410 | authzonepath = os.path.join(confdir, 'example.zone') | |
411 | with open(authzonepath, 'w') as authzone: | |
412 | authzone.write("""$ORIGIN example. | |
413 | @ 3600 IN SOA {soa} | |
414 | a 3600 IN A 192.0.2.42 | |
415 | b 3600 IN A 192.0.2.42 | |
416 | c 3600 IN A 192.0.2.42 | |
417 | d 3600 IN A 192.0.2.42 | |
418 | e 3600 IN A 192.0.2.42 | |
419 | """.format(soa=cls._SOA)) | |
420 | super(RPZRecursorTest, cls).generateRecursorConfig(confdir) | |
421 | ||
f9017ec1 RG |
422 | def waitUntilCorrectSerialIsLoaded(self, serial, timeout=5): |
423 | global rpzServer | |
424 | ||
425 | rpzServer.moveToSerial(serial) | |
426 | ||
427 | attempts = 0 | |
428 | while attempts < timeout: | |
429 | currentSerial = rpzServer.getCurrentSerial() | |
430 | if currentSerial > serial: | |
431 | raise AssertionError("Expected serial %d, got %d" % (serial, currentSerial)) | |
432 | if currentSerial == serial: | |
22cf3506 | 433 | self._xfrDone = self._xfrDone + 1 |
f9017ec1 RG |
434 | return |
435 | ||
436 | attempts = attempts + 1 | |
437 | time.sleep(1) | |
438 | ||
439 | raise AssertionError("Waited %d seconds for the serial to be updated to %d but the serial is still %d" % (timeout, serial, currentSerial)) | |
440 | ||
441 | def testRPZ(self): | |
046b69a5 | 442 | # Fresh RPZ does not need a notify |
43e11441 | 443 | self.waitForTCPSocket("127.0.0.1", self._wsPort) |
f9017ec1 RG |
444 | # first zone, only a should be blocked |
445 | self.waitUntilCorrectSerialIsLoaded(1) | |
22cf3506 | 446 | self.checkRPZStats(1, 1, 1, self._xfrDone) |
f6a524be | 447 | self.checkBlocked('a.example.', soa=True) |
f9017ec1 RG |
448 | self.checkNotBlocked('b.example.') |
449 | self.checkNotBlocked('c.example.') | |
450 | ||
451 | # second zone, a and b should be blocked | |
046b69a5 | 452 | self.sendNotify() |
f9017ec1 | 453 | self.waitUntilCorrectSerialIsLoaded(2) |
22cf3506 | 454 | self.checkRPZStats(2, 2, 1, self._xfrDone) |
f6a524be OM |
455 | self.checkBlocked('a.example.', soa=True) |
456 | self.checkBlocked('b.example.', soa=True) | |
f9017ec1 RG |
457 | self.checkNotBlocked('c.example.') |
458 | ||
459 | # third zone, only b should be blocked | |
046b69a5 | 460 | self.sendNotify() |
f9017ec1 | 461 | self.waitUntilCorrectSerialIsLoaded(3) |
22cf3506 | 462 | self.checkRPZStats(3, 1, 1, self._xfrDone) |
f9017ec1 | 463 | self.checkNotBlocked('a.example.') |
f6a524be | 464 | self.checkBlocked('b.example.', soa=True) |
f9017ec1 RG |
465 | self.checkNotBlocked('c.example.') |
466 | ||
467 | # fourth zone, only c should be blocked | |
046b69a5 | 468 | self.sendNotify() |
f9017ec1 | 469 | self.waitUntilCorrectSerialIsLoaded(4) |
22cf3506 | 470 | self.checkRPZStats(4, 1, 1, self._xfrDone) |
f9017ec1 RG |
471 | self.checkNotBlocked('a.example.') |
472 | self.checkNotBlocked('b.example.') | |
f6a524be | 473 | self.checkBlocked('c.example.', soa=True) |
22cf3506 RG |
474 | |
475 | # fifth zone, we should get a full AXFR this time, and only d should be blocked | |
046b69a5 | 476 | self.sendNotify() |
22cf3506 | 477 | self.waitUntilCorrectSerialIsLoaded(5) |
8340237f | 478 | self.checkRPZStats(5, 3, 2, self._xfrDone) |
22cf3506 RG |
479 | self.checkNotBlocked('a.example.') |
480 | self.checkNotBlocked('b.example.') | |
481 | self.checkNotBlocked('c.example.') | |
f6a524be | 482 | self.checkBlocked('d.example.', soa=True) |
22cf3506 | 483 | |
6da513b2 | 484 | # sixth zone, only e should be blocked, f is a local data record |
046b69a5 | 485 | self.sendNotify() |
22cf3506 | 486 | self.waitUntilCorrectSerialIsLoaded(6) |
6da513b2 RG |
487 | self.checkRPZStats(6, 2, 2, self._xfrDone) |
488 | self.checkNotBlocked('a.example.') | |
489 | self.checkNotBlocked('b.example.') | |
490 | self.checkNotBlocked('c.example.') | |
491 | self.checkNotBlocked('d.example.') | |
f6a524be | 492 | self.checkCustom('e.example.', 'A', dns.rrset.from_text('e.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.1', '192.0.2.2'), soa=True) |
6da513b2 | 493 | self.checkCustom('e.example.', 'MX', dns.rrset.from_text('e.example.', 0, dns.rdataclass.IN, 'MX', '10 mx.example.')) |
f6a524be OM |
494 | self.checkNoData('e.example.', 'AAAA', soa=True) |
495 | self.checkCustom('f.example.', 'A', dns.rrset.from_text('f.example.', 0, dns.rdataclass.IN, 'CNAME', 'e.example.'), soa=True) | |
6da513b2 RG |
496 | |
497 | # seventh zone, e should only have one A | |
046b69a5 | 498 | self.sendNotify() |
6da513b2 | 499 | self.waitUntilCorrectSerialIsLoaded(7) |
d13c4d18 | 500 | self.checkRPZStats(7, 4, 2, self._xfrDone) |
22cf3506 RG |
501 | self.checkNotBlocked('a.example.') |
502 | self.checkNotBlocked('b.example.') | |
503 | self.checkNotBlocked('c.example.') | |
504 | self.checkNotBlocked('d.example.') | |
f6a524be OM |
505 | self.checkCustom('e.example.', 'A', dns.rrset.from_text('e.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.2'), soa=True) |
506 | self.checkCustom('e.example.', 'MX', dns.rrset.from_text('e.example.', 0, dns.rdataclass.IN, 'MX', '10 mx.example.'), soa=True) | |
507 | self.checkNoData('e.example.', 'AAAA', soa=True) | |
508 | self.checkCustom('f.example.', 'A', dns.rrset.from_text('f.example.', 0, dns.rdataclass.IN, 'CNAME', 'e.example.'), soa=True) | |
d19bcbf0 RG |
509 | # check that the policy is disabled for AD=1 queries |
510 | self.checkNotBlocked('e.example.', True) | |
d13c4d18 | 511 | # check non-custom policies |
f6a524be | 512 | self.checkTruncated('tc.example.', soa=True) |
d13c4d18 | 513 | self.checkDropped('drop.example.') |
d122dac0 | 514 | |
98b33176 | 515 | # eighth zone, all entries should be gone |
046b69a5 | 516 | self.sendNotify() |
98b33176 RG |
517 | self.waitUntilCorrectSerialIsLoaded(8) |
518 | self.checkRPZStats(8, 0, 3, self._xfrDone) | |
519 | self.checkNotBlocked('a.example.') | |
520 | self.checkNotBlocked('b.example.') | |
521 | self.checkNotBlocked('c.example.') | |
522 | self.checkNotBlocked('d.example.') | |
523 | self.checkNotBlocked('e.example.') | |
524 | self.checkNXD('f.example.') | |
525 | self.checkNXD('tc.example.') | |
ee2a5356 RG |
526 | self.checkNXD('drop.example.') |
527 | ||
528 | # 9th zone is a duplicate, it might get skipped | |
529 | global rpzServer | |
530 | rpzServer.moveToSerial(9) | |
046b69a5 | 531 | self.sendNotify() |
ee2a5356 | 532 | time.sleep(3) |
046b69a5 | 533 | self.sendNotify() |
ee2a5356 RG |
534 | self.waitUntilCorrectSerialIsLoaded(10) |
535 | self.checkRPZStats(10, 1, 4, self._xfrDone) | |
536 | self.checkNotBlocked('a.example.') | |
537 | self.checkNotBlocked('b.example.') | |
538 | self.checkNotBlocked('c.example.') | |
539 | self.checkNotBlocked('d.example.') | |
540 | self.checkNotBlocked('e.example.') | |
f6a524be | 541 | self.checkBlocked('f.example.', soa=True) |
ee2a5356 | 542 | self.checkNXD('tc.example.') |
ba5f46ae RG |
543 | self.checkNXD('drop.example.') |
544 | ||
545 | # the next update will update the zone twice | |
546 | rpzServer.moveToSerial(11) | |
046b69a5 | 547 | self.sendNotify() |
ba5f46ae | 548 | time.sleep(3) |
046b69a5 | 549 | self.sendNotify() |
ba5f46ae RG |
550 | self.waitUntilCorrectSerialIsLoaded(12) |
551 | self.checkRPZStats(12, 1, 4, self._xfrDone) | |
552 | self.checkNotBlocked('a.example.') | |
553 | self.checkNotBlocked('b.example.') | |
554 | self.checkNotBlocked('c.example.') | |
555 | self.checkNotBlocked('d.example.') | |
556 | self.checkNotBlocked('e.example.') | |
557 | self.checkNXD('f.example.') | |
f6a524be | 558 | self.checkBlocked('g.example.', soa=True) |
ba5f46ae | 559 | self.checkNXD('tc.example.') |
98b33176 RG |
560 | self.checkNXD('drop.example.') |
561 | ||
d122dac0 RG |
562 | class RPZFileRecursorTest(RPZRecursorTest): |
563 | """ | |
564 | This test makes sure that we correctly load RPZ zones from a file | |
565 | """ | |
566 | ||
567 | _confdir = 'RPZFile' | |
d122dac0 | 568 | _lua_config_file = """ |
f6a524be | 569 | rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz.", includeSOA=true }) |
d122dac0 RG |
570 | """ % (_confdir) |
571 | _config_template = """ | |
572 | auth-zones=example=configs/%s/example.zone | |
f89ae456 | 573 | """ % (_confdir) |
d122dac0 RG |
574 | |
575 | @classmethod | |
576 | def generateRecursorConfig(cls, confdir): | |
577 | authzonepath = os.path.join(confdir, 'example.zone') | |
578 | with open(authzonepath, 'w') as authzone: | |
579 | authzone.write("""$ORIGIN example. | |
580 | @ 3600 IN SOA {soa} | |
581 | a 3600 IN A 192.0.2.42 | |
582 | b 3600 IN A 192.0.2.42 | |
583 | c 3600 IN A 192.0.2.42 | |
584 | d 3600 IN A 192.0.2.42 | |
585 | e 3600 IN A 192.0.2.42 | |
586 | z 3600 IN A 192.0.2.42 | |
587 | """.format(soa=cls._SOA)) | |
588 | ||
589 | rpzFilePath = os.path.join(confdir, 'zone.rpz') | |
590 | with open(rpzFilePath, 'w') as rpzZone: | |
591 | rpzZone.write("""$ORIGIN zone.rpz. | |
592 | @ 3600 IN SOA {soa} | |
593 | a.example.zone.rpz. 60 IN A 192.0.2.42 | |
594 | a.example.zone.rpz. 60 IN A 192.0.2.43 | |
595 | a.example.zone.rpz. 60 IN TXT "some text" | |
596 | drop.example.zone.rpz. 60 IN CNAME rpz-drop. | |
597 | z.example.zone.rpz. 60 IN A 192.0.2.1 | |
598 | tc.example.zone.rpz. 60 IN CNAME rpz-tcp-only. | |
599 | """.format(soa=cls._SOA)) | |
600 | super(RPZFileRecursorTest, cls).generateRecursorConfig(confdir) | |
601 | ||
602 | def testRPZ(self): | |
603 | self.checkCustom('a.example.', 'A', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.42', '192.0.2.43')) | |
604 | self.checkCustom('a.example.', 'TXT', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'TXT', '"some text"')) | |
f6a524be | 605 | self.checkBlocked('z.example.', soa=True) |
d122dac0 RG |
606 | self.checkNotBlocked('b.example.') |
607 | self.checkNotBlocked('c.example.') | |
608 | self.checkNotBlocked('d.example.') | |
609 | self.checkNotBlocked('e.example.') | |
610 | # check that the policy is disabled for AD=1 queries | |
611 | self.checkNotBlocked('z.example.', True) | |
612 | # check non-custom policies | |
f6a524be | 613 | self.checkTruncated('tc.example.', soa=True) |
d122dac0 RG |
614 | self.checkDropped('drop.example.') |
615 | ||
616 | class RPZFileDefaultPolRecursorTest(RPZRecursorTest): | |
617 | """ | |
618 | This test makes sure that we correctly load RPZ zones from a file with a default policy | |
619 | """ | |
620 | ||
621 | _confdir = 'RPZFileDefaultPolicy' | |
d122dac0 RG |
622 | _lua_config_file = """ |
623 | rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz.", defpol=Policy.NoAction }) | |
624 | """ % (_confdir) | |
625 | _config_template = """ | |
626 | auth-zones=example=configs/%s/example.zone | |
f89ae456 | 627 | """ % (_confdir) |
d122dac0 RG |
628 | |
629 | @classmethod | |
630 | def generateRecursorConfig(cls, confdir): | |
631 | authzonepath = os.path.join(confdir, 'example.zone') | |
632 | with open(authzonepath, 'w') as authzone: | |
633 | authzone.write("""$ORIGIN example. | |
634 | @ 3600 IN SOA {soa} | |
635 | a 3600 IN A 192.0.2.42 | |
636 | b 3600 IN A 192.0.2.42 | |
637 | c 3600 IN A 192.0.2.42 | |
638 | d 3600 IN A 192.0.2.42 | |
639 | drop 3600 IN A 192.0.2.42 | |
640 | e 3600 IN A 192.0.2.42 | |
641 | z 3600 IN A 192.0.2.42 | |
642 | """.format(soa=cls._SOA)) | |
643 | ||
644 | rpzFilePath = os.path.join(confdir, 'zone.rpz') | |
645 | with open(rpzFilePath, 'w') as rpzZone: | |
646 | rpzZone.write("""$ORIGIN zone.rpz. | |
647 | @ 3600 IN SOA {soa} | |
648 | a.example.zone.rpz. 60 IN A 192.0.2.42 | |
649 | drop.example.zone.rpz. 60 IN CNAME rpz-drop. | |
650 | z.example.zone.rpz. 60 IN A 192.0.2.1 | |
651 | tc.example.zone.rpz. 60 IN CNAME rpz-tcp-only. | |
652 | """.format(soa=cls._SOA)) | |
653 | super(RPZFileDefaultPolRecursorTest, cls).generateRecursorConfig(confdir) | |
654 | ||
655 | def testRPZ(self): | |
656 | # local data entries are overridden by default | |
657 | self.checkCustom('a.example.', 'A', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.42')) | |
658 | self.checkNoData('a.example.', 'TXT') | |
659 | # will not be blocked because the default policy overrides local data entries by default | |
660 | self.checkNotBlocked('z.example.') | |
661 | self.checkNotBlocked('b.example.') | |
662 | self.checkNotBlocked('c.example.') | |
663 | self.checkNotBlocked('d.example.') | |
664 | self.checkNotBlocked('e.example.') | |
665 | # check non-local policies, they should be overridden by the default policy | |
666 | self.checkNXD('tc.example.', 'A') | |
667 | self.checkNotBlocked('drop.example.') | |
668 | ||
669 | class RPZFileDefaultPolNotOverrideLocalRecursorTest(RPZRecursorTest): | |
670 | """ | |
671 | This test makes sure that we correctly load RPZ zones from a file with a default policy, not overriding local data entries | |
672 | """ | |
673 | ||
674 | _confdir = 'RPZFileDefaultPolicyNotOverrideLocal' | |
d122dac0 RG |
675 | _lua_config_file = """ |
676 | rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz.", defpol=Policy.NoAction, defpolOverrideLocalData=false }) | |
677 | """ % (_confdir) | |
678 | _config_template = """ | |
679 | auth-zones=example=configs/%s/example.zone | |
f89ae456 | 680 | """ % (_confdir) |
d122dac0 RG |
681 | |
682 | @classmethod | |
683 | def generateRecursorConfig(cls, confdir): | |
684 | authzonepath = os.path.join(confdir, 'example.zone') | |
685 | with open(authzonepath, 'w') as authzone: | |
686 | authzone.write("""$ORIGIN example. | |
687 | @ 3600 IN SOA {soa} | |
688 | a 3600 IN A 192.0.2.42 | |
689 | b 3600 IN A 192.0.2.42 | |
690 | c 3600 IN A 192.0.2.42 | |
691 | d 3600 IN A 192.0.2.42 | |
692 | drop 3600 IN A 192.0.2.42 | |
693 | e 3600 IN A 192.0.2.42 | |
694 | z 3600 IN A 192.0.2.42 | |
695 | """.format(soa=cls._SOA)) | |
696 | ||
697 | rpzFilePath = os.path.join(confdir, 'zone.rpz') | |
698 | with open(rpzFilePath, 'w') as rpzZone: | |
699 | rpzZone.write("""$ORIGIN zone.rpz. | |
700 | @ 3600 IN SOA {soa} | |
701 | a.example.zone.rpz. 60 IN A 192.0.2.42 | |
702 | a.example.zone.rpz. 60 IN A 192.0.2.43 | |
703 | a.example.zone.rpz. 60 IN TXT "some text" | |
704 | drop.example.zone.rpz. 60 IN CNAME rpz-drop. | |
705 | z.example.zone.rpz. 60 IN A 192.0.2.1 | |
706 | tc.example.zone.rpz. 60 IN CNAME rpz-tcp-only. | |
707 | """.format(soa=cls._SOA)) | |
708 | super(RPZFileDefaultPolNotOverrideLocalRecursorTest, cls).generateRecursorConfig(confdir) | |
709 | ||
710 | def testRPZ(self): | |
ef2ea4bf | 711 | # local data entries will not be overridden by the default policy |
d122dac0 RG |
712 | self.checkCustom('a.example.', 'A', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.42', '192.0.2.43')) |
713 | self.checkCustom('a.example.', 'TXT', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'TXT', '"some text"')) | |
714 | # will be blocked because the default policy does not override local data entries | |
715 | self.checkBlocked('z.example.') | |
716 | self.checkNotBlocked('b.example.') | |
717 | self.checkNotBlocked('c.example.') | |
718 | self.checkNotBlocked('d.example.') | |
719 | self.checkNotBlocked('e.example.') | |
720 | # check non-local policies, they should be overridden by the default policy | |
721 | self.checkNXD('tc.example.', 'A') | |
722 | self.checkNotBlocked('drop.example.') | |
1d2777e9 | 723 | |
f89ae456 RG |
724 | class RPZSimpleAuthServer(object): |
725 | ||
726 | def __init__(self, port): | |
727 | self._serverPort = port | |
728 | listener = threading.Thread(name='RPZ Simple Auth Listener', target=self._listener, args=[]) | |
046b69a5 | 729 | listener.daemon = True |
f89ae456 RG |
730 | listener.start() |
731 | ||
732 | def _getAnswer(self, message): | |
733 | ||
734 | response = dns.message.make_response(message) | |
735 | response.flags |= dns.flags.AA | |
736 | records = [ | |
737 | dns.rrset.from_text('nsip.delegated.example.', 60, dns.rdataclass.IN, dns.rdatatype.A, '192.0.2.42') | |
738 | ] | |
739 | ||
740 | response.answer = records | |
741 | return response | |
742 | ||
743 | def _listener(self): | |
744 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) | |
745 | try: | |
746 | sock.bind(("127.0.0.1", self._serverPort)) | |
747 | except socket.error as e: | |
748 | print("Error binding in the RPZ simple auth listener: %s" % str(e)) | |
749 | sys.exit(1) | |
750 | ||
751 | while True: | |
752 | try: | |
753 | data, addr = sock.recvfrom(4096) | |
754 | message = dns.message.from_wire(data) | |
755 | if len(message.question) != 1: | |
756 | print('Invalid query, qdcount is %d' % (len(message.question))) | |
757 | break | |
758 | ||
759 | answer = self._getAnswer(message) | |
760 | if not answer: | |
761 | print('Unable to get a response for %s %d' % (message.question[0].name, message.question[0].rdtype)) | |
762 | break | |
763 | ||
764 | wire = answer.to_wire() | |
765 | sock.sendto(wire, addr) | |
766 | ||
767 | except socket.error as e: | |
768 | print('Error in RPZ simple auth socket: %s' % str(e)) | |
769 | ||
770 | rpzAuthServerPort = 4260 | |
771 | rpzAuthServer = RPZSimpleAuthServer(rpzAuthServerPort) | |
772 | ||
773 | class RPZOrderingPrecedenceRecursorTest(RPZRecursorTest): | |
1d2777e9 RG |
774 | """ |
775 | This test makes sure that the recursor respects the RPZ ordering precedence rules | |
776 | """ | |
777 | ||
778 | _confdir = 'RPZOrderingPrecedence' | |
1d2777e9 RG |
779 | _lua_config_file = """ |
780 | rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz."}) | |
781 | rpzFile('configs/%s/zone2.rpz', { policyName="zone2.rpz."}) | |
782 | """ % (_confdir, _confdir) | |
783 | _config_template = """ | |
784 | auth-zones=example=configs/%s/example.zone | |
f89ae456 RG |
785 | forward-zones=delegated.example=127.0.0.1:%d |
786 | """ % (_confdir, rpzAuthServerPort) | |
1d2777e9 RG |
787 | |
788 | @classmethod | |
789 | def generateRecursorConfig(cls, confdir): | |
790 | authzonepath = os.path.join(confdir, 'example.zone') | |
791 | with open(authzonepath, 'w') as authzone: | |
792 | authzone.write("""$ORIGIN example. | |
793 | @ 3600 IN SOA {soa} | |
794 | sub.test 3600 IN A 192.0.2.42 | |
fa973749 RG |
795 | passthru-then-blocked-by-higher 3600 IN A 192.0.2.66 |
796 | passthru-then-blocked-by-same 3600 IN A 192.0.2.66 | |
797 | blocked-then-passhtru-by-higher 3600 IN A 192.0.2.100 | |
1d2777e9 RG |
798 | """.format(soa=cls._SOA)) |
799 | ||
800 | rpzFilePath = os.path.join(confdir, 'zone.rpz') | |
801 | with open(rpzFilePath, 'w') as rpzZone: | |
802 | rpzZone.write("""$ORIGIN zone.rpz. | |
803 | @ 3600 IN SOA {soa} | |
804 | *.test.example.zone.rpz. 60 IN CNAME rpz-passthru. | |
fa973749 RG |
805 | 32.66.2.0.192.rpz-ip.zone.rpz. 60 IN A 192.0.2.1 |
806 | 32.100.2.0.192.rpz-ip.zone.rpz. 60 IN CNAME rpz-passthru. | |
807 | passthru-then-blocked-by-same.example.zone.rpz. 60 IN CNAME rpz-passthru. | |
f89ae456 | 808 | 32.1.0.0.127.rpz-nsip.zone.rpz. 60 IN CNAME rpz-passthru. |
1d2777e9 RG |
809 | """.format(soa=cls._SOA)) |
810 | ||
811 | rpzFilePath = os.path.join(confdir, 'zone2.rpz') | |
812 | with open(rpzFilePath, 'w') as rpzZone: | |
813 | rpzZone.write("""$ORIGIN zone2.rpz. | |
814 | @ 3600 IN SOA {soa} | |
815 | sub.test.example.com.zone2.rpz. 60 IN CNAME . | |
fa973749 RG |
816 | passthru-then-blocked-by-higher.example.zone2.rpz. 60 IN CNAME rpz-passthru. |
817 | blocked-then-passhtru-by-higher.example.zone2.rpz. 60 IN A 192.0.2.1 | |
1d2777e9 RG |
818 | 32.42.2.0.192.rpz-ip 60 IN CNAME . |
819 | """.format(soa=cls._SOA)) | |
820 | ||
f89ae456 | 821 | super(RPZOrderingPrecedenceRecursorTest, cls).generateRecursorConfig(confdir) |
1d2777e9 | 822 | |
fa973749 | 823 | def testRPZOrderingForQNameAndWhitelisting(self): |
1d2777e9 RG |
824 | # we should first match on the qname (the wildcard, not on the exact name since |
825 | # we respect the order of the RPZ zones), see the pass-thru rule | |
fa973749 RG |
826 | # and only process RPZ rules of higher precedence. |
827 | # The subsequent rule on the content of the A should therefore not trigger a NXDOMAIN. | |
1d2777e9 | 828 | self.checkNotBlocked('sub.test.example.') |
fa973749 RG |
829 | |
830 | def testRPZOrderingWhitelistedThenBlockedByHigher(self): | |
831 | # we should first match on the qname from the second RPZ zone, | |
832 | # continue the resolution process, and get blocked by the content of the A record | |
833 | # based on the first RPZ zone, whose priority is higher than the second one. | |
834 | self.checkBlocked('passthru-then-blocked-by-higher.example.') | |
835 | ||
836 | def testRPZOrderingWhitelistedThenBlockedBySame(self): | |
837 | # we should first match on the qname from the first RPZ zone, | |
838 | # continue the resolution process, and NOT get blocked by the content of the A record | |
839 | # based on the same RPZ zone, since it's not higher. | |
840 | self.checkCustom('passthru-then-blocked-by-same.example.', 'A', dns.rrset.from_text('passthru-then-blocked-by-same.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.66')) | |
841 | ||
842 | def testRPZOrderBlockedThenWhitelisted(self): | |
843 | # The qname is first blocked by the second RPZ zone | |
844 | # Then, should the resolution process go on, the A record would be whitelisted | |
845 | # by the first zone. | |
846 | # This is what the RPZ specification requires, but we currently decided that we | |
847 | # don't want to leak queries to malicious DNS servers and waste time if the qname is blacklisted. | |
848 | # We might change our opinion at some point, though. | |
849 | self.checkBlocked('blocked-then-passhtru-by-higher.example.') | |
f89ae456 RG |
850 | |
851 | def testRPZOrderDelegate(self): | |
852 | # The IP of the NS we are going to contact is whitelisted (passthru) in zone 1, | |
853 | # so even though the record (192.0.2.42) returned by the server is blacklisted | |
854 | # by zone 2, it should not be blocked. | |
855 | # We only test once because after that the answer is cached, so the NS is not contacted | |
856 | # and the whitelist is not applied (yes, NSIP and NSDNAME are brittle). | |
857 | self.checkNotBlocked('nsip.delegated.example.', singleCheck=True) | |
858 | ||
859 | class RPZNSIPCustomTest(RPZRecursorTest): | |
860 | """ | |
861 | This test makes sure that the recursor handles custom RPZ rules in a NSIP | |
862 | """ | |
863 | ||
864 | _confdir = 'RPZNSIPCustom' | |
865 | _lua_config_file = """ | |
866 | rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz."}) | |
867 | rpzFile('configs/%s/zone2.rpz', { policyName="zone2.rpz."}) | |
868 | """ % (_confdir, _confdir) | |
869 | _config_template = """ | |
870 | auth-zones=example=configs/%s/example.zone | |
871 | forward-zones=delegated.example=127.0.0.1:%d | |
872 | """ % (_confdir, rpzAuthServerPort) | |
873 | ||
874 | @classmethod | |
875 | def generateRecursorConfig(cls, confdir): | |
876 | authzonepath = os.path.join(confdir, 'example.zone') | |
877 | with open(authzonepath, 'w') as authzone: | |
878 | authzone.write("""$ORIGIN example. | |
879 | @ 3600 IN SOA {soa} | |
880 | """.format(soa=cls._SOA)) | |
881 | ||
882 | rpzFilePath = os.path.join(confdir, 'zone.rpz') | |
883 | with open(rpzFilePath, 'w') as rpzZone: | |
884 | rpzZone.write("""$ORIGIN zone.rpz. | |
885 | @ 3600 IN SOA {soa} | |
886 | 32.1.0.0.127.rpz-nsip.zone.rpz. 60 IN A 192.0.2.1 | |
887 | """.format(soa=cls._SOA)) | |
888 | ||
889 | rpzFilePath = os.path.join(confdir, 'zone2.rpz') | |
890 | with open(rpzFilePath, 'w') as rpzZone: | |
891 | rpzZone.write("""$ORIGIN zone2.rpz. | |
892 | @ 3600 IN SOA {soa} | |
893 | 32.1.2.0.192.rpz-ip 60 IN CNAME . | |
894 | """.format(soa=cls._SOA)) | |
895 | ||
896 | super(RPZNSIPCustomTest, cls).generateRecursorConfig(confdir) | |
897 | ||
898 | def testRPZDelegate(self): | |
899 | # The IP of the NS we are going to contact should result in a custom record (192.0.2.1) from zone 1, | |
900 | # so even though the record (192.0.2.1) returned by the server is blacklisted | |
901 | # by zone 2, it should not be blocked. | |
902 | # We only test once because after that the answer is cached, so the NS is not contacted | |
903 | # and the whitelist is not applied (yes, NSIP and NSDNAME are brittle). | |
904 | self.checkCustom('nsip.delegated.example.', 'A', dns.rrset.from_text('nsip.delegated.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.1')) | |
b7284b4d RG |
905 | |
906 | ||
907 | class RPZResponseIPCNameChainCustomTest(RPZRecursorTest): | |
908 | """ | |
909 | This test makes sure that the recursor applies response IP rules to records in a CNAME chain, | |
910 | and resolves the target of a custom CNAME. | |
911 | """ | |
912 | ||
913 | _confdir = 'RPZResponseIPCNameChainCustom' | |
914 | _lua_config_file = """ | |
915 | rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz."}) | |
916 | """ % (_confdir) | |
917 | _config_template = """ | |
918 | auth-zones=example=configs/%s/example.zone | |
919 | forward-zones=delegated.example=127.0.0.1:%d | |
920 | """ % (_confdir, rpzAuthServerPort) | |
921 | ||
922 | @classmethod | |
923 | def generateRecursorConfig(cls, confdir): | |
924 | authzonepath = os.path.join(confdir, 'example.zone') | |
925 | with open(authzonepath, 'w') as authzone: | |
926 | authzone.write("""$ORIGIN example. | |
927 | @ 3600 IN SOA {soa} | |
928 | name IN CNAME cname | |
929 | cname IN A 192.0.2.255 | |
930 | custom-target IN A 192.0.2.254 | |
931 | """.format(soa=cls._SOA)) | |
932 | ||
933 | rpzFilePath = os.path.join(confdir, 'zone.rpz') | |
934 | with open(rpzFilePath, 'w') as rpzZone: | |
935 | rpzZone.write("""$ORIGIN zone.rpz. | |
936 | @ 3600 IN SOA {soa} | |
937 | cname.example IN CNAME custom-target.example. | |
938 | custom-target.example IN A 192.0.2.253 | |
939 | """.format(soa=cls._SOA)) | |
940 | ||
941 | super(RPZResponseIPCNameChainCustomTest, cls).generateRecursorConfig(confdir) | |
942 | ||
943 | def testRPZChain(self): | |
944 | # we request the A record for 'name.example.', which is a CNAME to 'cname.example' | |
945 | # this one does exist but we have a RPZ rule that should be triggered, | |
946 | # replacing the 'real' CNAME by a CNAME to 'custom-target.example.' | |
947 | # There is a RPZ rule for that name but it should not be triggered, since | |
948 | # the RPZ specs state "Recall that only one policy rule, from among all those matched at all | |
949 | # stages of resolving a CNAME or DNAME chain, can affect the final | |
950 | # response; this is true even if the selected rule has a PASSTHRU | |
951 | # action" in 5.1 "CNAME or DNAME Chain Position" Precedence Rule | |
952 | ||
953 | # two times to check the cache | |
954 | for _ in range(2): | |
955 | query = dns.message.make_query('name.example.', 'A', want_dnssec=True) | |
956 | query.flags |= dns.flags.CD | |
957 | for method in ("sendUDPQuery", "sendTCPQuery"): | |
958 | sender = getattr(self, method) | |
959 | res = sender(query) | |
960 | self.assertRcodeEqual(res, dns.rcode.NOERROR) | |
961 | self.assertRRsetInAnswer(res, dns.rrset.from_text('name.example.', 0, dns.rdataclass.IN, 'CNAME', 'cname.example.')) | |
962 | self.assertRRsetInAnswer(res, dns.rrset.from_text('cname.example.', 0, dns.rdataclass.IN, 'CNAME', 'custom-target.example.')) | |
963 | self.assertRRsetInAnswer(res, dns.rrset.from_text('custom-target.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.254')) | |
964 | ||
965 | ||
966 | class RPZCNameChainCustomTest(RPZRecursorTest): | |
967 | """ | |
968 | This test makes sure that the recursor applies QName rules to names in a CNAME chain. | |
969 | No forward or internal auth zones here, as we want to test the real resolution | |
970 | (with QName Minimization). | |
971 | """ | |
972 | ||
973 | _PREFIX = os.environ['PREFIX'] | |
974 | _confdir = 'RPZCNameChainCustom' | |
975 | _lua_config_file = """ | |
976 | rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz."}) | |
977 | """ % (_confdir) | |
978 | _config_template = "" | |
979 | ||
b7284b4d RG |
980 | @classmethod |
981 | def generateRecursorConfig(cls, confdir): | |
982 | rpzFilePath = os.path.join(confdir, 'zone.rpz') | |
983 | with open(rpzFilePath, 'w') as rpzZone: | |
984 | rpzZone.write("""$ORIGIN zone.rpz. | |
985 | @ 3600 IN SOA {soa} | |
986 | 32.100.2.0.192.rpz-ip IN CNAME . | |
987 | 32.101.2.0.192.rpz-ip IN CNAME *. | |
988 | 32.102.2.0.192.rpz-ip IN A 192.0.2.103 | |
989 | """.format(soa=cls._SOA)) | |
990 | ||
991 | super(RPZCNameChainCustomTest, cls).generateRecursorConfig(confdir) | |
992 | ||
993 | def testRPZChainNXD(self): | |
994 | # we should match the A at the end of the CNAME chain and | |
995 | # trigger a NXD | |
996 | ||
997 | # two times to check the cache | |
998 | for _ in range(2): | |
999 | query = dns.message.make_query('cname-nxd.example.', 'A', want_dnssec=True) | |
1000 | query.flags |= dns.flags.CD | |
1001 | for method in ("sendUDPQuery", "sendTCPQuery"): | |
1002 | sender = getattr(self, method) | |
1003 | res = sender(query) | |
1004 | self.assertRcodeEqual(res, dns.rcode.NXDOMAIN) | |
4bfebc93 | 1005 | self.assertEqual(len(res.answer), 0) |
b7284b4d RG |
1006 | |
1007 | def testRPZChainNODATA(self): | |
1008 | # we should match the A at the end of the CNAME chain and | |
1009 | # trigger a NODATA | |
1010 | ||
1011 | # two times to check the cache | |
1012 | for _ in range(2): | |
1013 | query = dns.message.make_query('cname-nodata.example.', 'A', want_dnssec=True) | |
1014 | query.flags |= dns.flags.CD | |
1015 | for method in ("sendUDPQuery", "sendTCPQuery"): | |
1016 | sender = getattr(self, method) | |
1017 | res = sender(query) | |
1018 | self.assertRcodeEqual(res, dns.rcode.NOERROR) | |
4bfebc93 | 1019 | self.assertEqual(len(res.answer), 0) |
b7284b4d RG |
1020 | |
1021 | def testRPZChainCustom(self): | |
1022 | # we should match the A at the end of the CNAME chain and | |
1023 | # get a custom A, replacing the existing one | |
1024 | ||
1025 | # two times to check the cache | |
1026 | for _ in range(2): | |
1027 | query = dns.message.make_query('cname-custom-a.example.', 'A', want_dnssec=True) | |
1028 | query.flags |= dns.flags.CD | |
1029 | for method in ("sendUDPQuery", "sendTCPQuery"): | |
1030 | sender = getattr(self, method) | |
1031 | res = sender(query) | |
1032 | self.assertRcodeEqual(res, dns.rcode.NOERROR) | |
1033 | # the original CNAME record is signed | |
4bfebc93 | 1034 | self.assertEqual(len(res.answer), 3) |
b7284b4d RG |
1035 | self.assertRRsetInAnswer(res, dns.rrset.from_text('cname-custom-a.example.', 0, dns.rdataclass.IN, 'CNAME', 'cname-custom-a-target.example.')) |
1036 | self.assertRRsetInAnswer(res, dns.rrset.from_text('cname-custom-a-target.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.103')) | |
5fb5beff O |
1037 | |
1038 | class RPZFileModByLuaRecursorTest(RPZRecursorTest): | |
1039 | """ | |
1040 | This test makes sure that we correctly load RPZ zones from a file while being modified by Lua callbacks | |
1041 | """ | |
1042 | ||
1043 | _confdir = 'RPZFileModByLua' | |
1044 | _lua_dns_script_file = """ | |
1045 | function preresolve(dq) | |
1046 | if dq.qname:equal('zmod.example.') then | |
1047 | dq.appliedPolicy.policyKind = pdns.policykinds.Drop | |
1048 | return true | |
1049 | end | |
1050 | return false | |
1051 | end | |
1052 | function nxdomain(dq) | |
1053 | if dq.qname:equal('nxmod.example.') then | |
1054 | dq.appliedPolicy.policyKind = pdns.policykinds.Drop | |
1055 | return true | |
1056 | end | |
1057 | return false | |
1058 | end | |
1059 | function nodata(dq) | |
1060 | print("NODATA") | |
1061 | if dq.qname:equal('nodatamod.example.') then | |
1062 | dq.appliedPolicy.policyKind = pdns.policykinds.Drop | |
1063 | return true | |
1064 | end | |
1065 | return false | |
1066 | end | |
1067 | """ | |
1068 | _lua_config_file = """ | |
1069 | rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz." }) | |
1070 | """ % (_confdir) | |
1071 | _config_template = """ | |
1072 | auth-zones=example=configs/%s/example.zone | |
1073 | """ % (_confdir) | |
1074 | ||
1075 | @classmethod | |
1076 | def generateRecursorConfig(cls, confdir): | |
1077 | authzonepath = os.path.join(confdir, 'example.zone') | |
1078 | with open(authzonepath, 'w') as authzone: | |
1079 | authzone.write("""$ORIGIN example. | |
1080 | @ 3600 IN SOA {soa} | |
1081 | a 3600 IN A 192.0.2.42 | |
1082 | b 3600 IN A 192.0.2.42 | |
1083 | c 3600 IN A 192.0.2.42 | |
1084 | d 3600 IN A 192.0.2.42 | |
1085 | e 3600 IN A 192.0.2.42 | |
1086 | z 3600 IN A 192.0.2.42 | |
1087 | """.format(soa=cls._SOA)) | |
1088 | ||
1089 | rpzFilePath = os.path.join(confdir, 'zone.rpz') | |
1090 | with open(rpzFilePath, 'w') as rpzZone: | |
1091 | rpzZone.write("""$ORIGIN zone.rpz. | |
1092 | @ 3600 IN SOA {soa} | |
1093 | a.example.zone.rpz. 60 IN A 192.0.2.42 | |
1094 | a.example.zone.rpz. 60 IN A 192.0.2.43 | |
1095 | a.example.zone.rpz. 60 IN TXT "some text" | |
1096 | drop.example.zone.rpz. 60 IN CNAME rpz-drop. | |
1097 | zmod.example.zone.rpz. 60 IN A 192.0.2.1 | |
1098 | tc.example.zone.rpz. 60 IN CNAME rpz-tcp-only. | |
1099 | nxmod.exmaple.zone.rpz. 60 in CNAME . | |
1100 | nodatamod.example.zone.rpz. 60 in CNAME *. | |
1101 | """.format(soa=cls._SOA)) | |
1102 | super(RPZFileModByLuaRecursorTest, cls).generateRecursorConfig(confdir) | |
1103 | ||
1104 | def testRPZ(self): | |
1105 | self.checkCustom('a.example.', 'A', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'A', '192.0.2.42', '192.0.2.43')) | |
1106 | self.checkCustom('a.example.', 'TXT', dns.rrset.from_text('a.example.', 0, dns.rdataclass.IN, 'TXT', '"some text"')) | |
1107 | self.checkDropped('zmod.example.') | |
1108 | self.checkDropped('nxmod.example.') | |
1109 | self.checkDropped('nodatamod.example.') | |
1110 | self.checkNotBlocked('b.example.') | |
1111 | self.checkNotBlocked('c.example.') | |
1112 | self.checkNotBlocked('d.example.') | |
1113 | self.checkNotBlocked('e.example.') | |
1114 | # check non-custom policies | |
1115 | self.checkTruncated('tc.example.') | |
1116 | self.checkDropped('drop.example.') |