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