11 from recursortests
import RecursorTest
13 class RPZServer(object):
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)
23 def getCurrentSerial(self
):
24 return self
._currentSerial
26 def moveToSerial(self
, newSerial
):
27 if newSerial
== self
._currentSerial
:
30 if newSerial
!= self
._currentSerial
+ 1:
31 raise AssertionError("Asking the RPZ server to server serial %d, already serving %d" % (newSerial
, self
._currentSerial
))
32 self
._targetSerial
= newSerial
35 def _getAnswer(self
, message
):
37 response
= dns
.message
.make_response(message
)
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
)
45 newSerial
= self
._targetSerial
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
)
52 elif message
.question
[0].rdtype
== dns
.rdatatype
.IXFR
:
53 oldSerial
= message
.authority
[0][0].serial
55 if oldSerial
!= self
._currentSerial
:
56 print('Received an IXFR query with an unexpected serial %d, expected %d' % (oldSerial
, self
._currentSerial
))
57 return (None, self
._currentSerial
)
59 newSerial
= self
._targetSerial
62 dns
.rrset
.from_text('zone.rpz.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.SOA
, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial
),
63 dns
.rrset
.from_text('zone.rpz.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.SOA
, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % oldSerial
),
65 dns
.rrset
.from_text('zone.rpz.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.SOA
, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial
),
66 dns
.rrset
.from_text('b.example.zone.rpz.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.A
, '192.0.2.1'),
67 dns
.rrset
.from_text('zone.rpz.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.SOA
, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial
)
71 dns
.rrset
.from_text('zone.rpz.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.SOA
, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial
),
72 dns
.rrset
.from_text('zone.rpz.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.SOA
, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % oldSerial
),
73 dns
.rrset
.from_text('a.example.zone.rpz.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.A
, '192.0.2.1'),
74 dns
.rrset
.from_text('zone.rpz.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.SOA
, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial
),
76 dns
.rrset
.from_text('zone.rpz.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.SOA
, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial
)
80 dns
.rrset
.from_text('zone.rpz.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.SOA
, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial
),
81 dns
.rrset
.from_text('zone.rpz.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.SOA
, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % oldSerial
),
82 dns
.rrset
.from_text('b.example.zone.rpz.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.A
, '192.0.2.1'),
83 dns
.rrset
.from_text('zone.rpz.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.SOA
, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial
),
84 dns
.rrset
.from_text('c.example.zone.rpz.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.A
, '192.0.2.1'),
85 dns
.rrset
.from_text('zone.rpz.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.SOA
, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial
)
88 # this one is a bit special, we are answering with a full AXFR
90 dns
.rrset
.from_text('zone.rpz.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.SOA
, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial
),
91 dns
.rrset
.from_text('d.example.zone.rpz.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.A
, '192.0.2.1'),
92 dns
.rrset
.from_text('tc.example.zone.rpz.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.CNAME
, 'rpz-tcp-only.'),
93 dns
.rrset
.from_text('drop.example.zone.rpz.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.CNAME
, 'rpz-drop.'),
94 dns
.rrset
.from_text('zone.rpz.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.SOA
, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial
)
99 dns
.rrset
.from_text('zone.rpz.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.SOA
, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial
),
100 dns
.rrset
.from_text('zone.rpz.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.SOA
, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % oldSerial
),
101 dns
.rrset
.from_text('d.example.zone.rpz.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.A
, '192.0.2.1'),
102 dns
.rrset
.from_text('tc.example.zone.rpz.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.CNAME
, 'rpz-tcp-only.'),
103 dns
.rrset
.from_text('drop.example.zone.rpz.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.CNAME
, 'rpz-drop.'),
104 dns
.rrset
.from_text('zone.rpz.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.SOA
, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial
),
105 dns
.rrset
.from_text('e.example.zone.rpz.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.A
, '192.0.2.1', '192.0.2.2'),
106 dns
.rrset
.from_text('e.example.zone.rpz.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.MX
, '10 mx.example.'),
107 dns
.rrset
.from_text('f.example.zone.rpz.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.CNAME
, 'e.example.'),
108 dns
.rrset
.from_text('zone.rpz.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.SOA
, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial
)
112 dns
.rrset
.from_text('zone.rpz.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.SOA
, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial
),
113 dns
.rrset
.from_text('zone.rpz.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.SOA
, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % oldSerial
),
114 dns
.rrset
.from_text('e.example.zone.rpz.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.A
, '192.0.2.1', '192.0.2.2'),
115 dns
.rrset
.from_text('zone.rpz.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.SOA
, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial
),
116 dns
.rrset
.from_text('e.example.zone.rpz.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.A
, '192.0.2.2'),
117 dns
.rrset
.from_text('tc.example.zone.rpz.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.CNAME
, 'rpz-tcp-only.'),
118 dns
.rrset
.from_text('drop.example.zone.rpz.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.CNAME
, 'rpz-drop.'),
119 dns
.rrset
.from_text('zone.rpz.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.SOA
, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial
)
122 # this one is a bit special too, we are answering with a full AXFR and the new zone is empty
124 dns
.rrset
.from_text('zone.rpz.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.SOA
, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial
),
125 dns
.rrset
.from_text('zone.rpz.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.SOA
, 'ns.zone.rpz. hostmaster.zone.rpz. %d 3600 3600 3600 1' % newSerial
)
128 response
.answer
= records
129 return (newSerial
, response
)
131 def _connectionHandler(self
, conn
):
137 (datalen
,) = struct
.unpack("!H", data
)
138 data
= conn
.recv(datalen
)
142 message
= dns
.message
.from_wire(data
)
143 if len(message
.question
) != 1:
144 print('Invalid RPZ query, qdcount is %d' % (len(message
.question
)))
146 if not message
.question
[0].rdtype
in [dns
.rdatatype
.AXFR
, dns
.rdatatype
.IXFR
]:
147 print('Invalid RPZ query, qtype is %d' % (message
.question
.rdtype
))
149 (serial
, answer
) = self
._getAnswer
(message
)
151 print('Unable to get a response for %s %d' % (message
.question
[0].name
, message
.question
[0].rdtype
))
154 wire
= answer
.to_wire()
155 conn
.send(struct
.pack("!H", len(wire
)))
157 self
._currentSerial
= serial
163 sock
= socket
.socket(socket
.AF_INET
, socket
.SOCK_STREAM
)
164 sock
.setsockopt(socket
.SOL_SOCKET
, socket
.SO_REUSEPORT
, 1)
166 sock
.bind(("127.0.0.1", self
._serverPort
))
167 except socket
.error
as e
:
168 print("Error binding in the RPZ listener: %s" % str(e
))
174 (conn
, _
) = sock
.accept()
175 thread
= threading
.Thread(name
='RPZ Connection Handler',
176 target
=self
._connectionHandler
,
178 thread
.setDaemon(True)
181 except socket
.error
as e
:
182 print('Error in RPZ socket: %s' % str(e
))
185 class RPZRecursorTest(RecursorTest
):
188 _wsPassword
= 'secretpassword'
189 _apiKey
= 'secretapikey'
191 _lua_dns_script_file
= """
194 -- disable the RPZ policy named 'zone.rpz' for AD=1 queries
195 if dq:getDH():getAD() then
196 dq:discardPolicy('zone.rpz.')
202 _config_template
= """
203 auth-zones=example=configs/%s/example.zone
206 webserver-address=127.0.0.1
207 webserver-password=%s
210 """ % (_confdir
, _wsPort
, _wsPassword
, _apiKey
)
216 cls
.startResponders()
218 confdir
= os
.path
.join('configs', cls
._confdir
)
219 cls
.createConfigDir(confdir
)
221 cls
.generateRecursorConfig(confdir
)
222 cls
.startRecursor(confdir
, cls
._recursorPort
)
225 def tearDownClass(cls
):
226 cls
.tearDownRecursor()
228 def checkBlocked(self
, name
, shouldBeBlocked
=True, adQuery
=False, singleCheck
=False):
229 query
= dns
.message
.make_query(name
, 'A', want_dnssec
=True)
230 query
.flags |
= dns
.flags
.CD
232 query
.flags |
= dns
.flags
.AD
234 for method
in ("sendUDPQuery", "sendTCPQuery"):
235 sender
= getattr(self
, method
)
237 self
.assertRcodeEqual(res
, dns
.rcode
.NOERROR
)
239 expected
= dns
.rrset
.from_text(name
, 0, dns
.rdataclass
.IN
, 'A', '192.0.2.1')
241 expected
= dns
.rrset
.from_text(name
, 0, dns
.rdataclass
.IN
, 'A', '192.0.2.42')
243 self
.assertRRsetInAnswer(res
, expected
)
247 def checkNotBlocked(self
, name
, adQuery
=False, singleCheck
=False):
248 self
.checkBlocked(name
, False, adQuery
, singleCheck
)
250 def checkCustom(self
, qname
, qtype
, expected
):
251 query
= dns
.message
.make_query(qname
, qtype
, want_dnssec
=True)
252 query
.flags |
= dns
.flags
.CD
253 for method
in ("sendUDPQuery", "sendTCPQuery"):
254 sender
= getattr(self
, method
)
256 self
.assertRcodeEqual(res
, dns
.rcode
.NOERROR
)
257 self
.assertRRsetInAnswer(res
, expected
)
259 def checkNoData(self
, qname
, qtype
):
260 query
= dns
.message
.make_query(qname
, qtype
, want_dnssec
=True)
261 query
.flags |
= dns
.flags
.CD
262 for method
in ("sendUDPQuery", "sendTCPQuery"):
263 sender
= getattr(self
, method
)
265 self
.assertRcodeEqual(res
, dns
.rcode
.NOERROR
)
266 self
.assertEqual(len(res
.answer
), 0)
268 def checkNXD(self
, qname
, qtype
='A'):
269 query
= dns
.message
.make_query(qname
, qtype
, want_dnssec
=True)
270 query
.flags |
= dns
.flags
.CD
271 for method
in ("sendUDPQuery", "sendTCPQuery"):
272 sender
= getattr(self
, method
)
274 self
.assertRcodeEqual(res
, dns
.rcode
.NXDOMAIN
)
275 self
.assertEqual(len(res
.answer
), 0)
276 self
.assertEqual(len(res
.authority
), 1)
278 def checkTruncated(self
, qname
, qtype
='A'):
279 query
= dns
.message
.make_query(qname
, qtype
, want_dnssec
=True)
280 query
.flags |
= dns
.flags
.CD
281 res
= self
.sendUDPQuery(query
)
282 self
.assertRcodeEqual(res
, dns
.rcode
.NOERROR
)
283 self
.assertMessageHasFlags(res
, ['QR', 'RA', 'RD', 'CD', 'TC'])
284 self
.assertEqual(len(res
.answer
), 0)
285 self
.assertEqual(len(res
.authority
), 0)
286 self
.assertEqual(len(res
.additional
), 0)
288 res
= self
.sendTCPQuery(query
)
289 self
.assertRcodeEqual(res
, dns
.rcode
.NXDOMAIN
)
290 self
.assertMessageHasFlags(res
, ['QR', 'RA', 'RD', 'CD'])
291 self
.assertEqual(len(res
.answer
), 0)
292 self
.assertEqual(len(res
.authority
), 1)
293 self
.assertEqual(len(res
.additional
), 0)
295 def checkDropped(self
, qname
, qtype
='A'):
296 query
= dns
.message
.make_query(qname
, qtype
, want_dnssec
=True)
297 query
.flags |
= dns
.flags
.CD
298 for method
in ("sendUDPQuery", "sendTCPQuery"):
299 sender
= getattr(self
, method
)
301 self
.assertEqual(res
, None)
303 def checkRPZStats(self
, serial
, recordsCount
, fullXFRCount
, totalXFRCount
):
304 headers
= {'x-api-key': self
._apiKey
}
305 url
= 'http://127.0.0.1:' + str(self
._wsPort
) + '/api/v1/servers/localhost/rpzstatistics'
306 r
= requests
.get(url
, headers
=headers
, timeout
=self
._wsTimeout
)
308 self
.assertEquals(r
.status_code
, 200)
309 self
.assertTrue(r
.json())
311 self
.assertIn('zone.rpz.', content
)
312 zone
= content
['zone.rpz.']
313 for key
in ['last_update', 'records', 'serial', 'transfers_failed', 'transfers_full', 'transfers_success']:
314 self
.assertIn(key
, zone
)
316 self
.assertEquals(zone
['serial'], serial
)
317 self
.assertEquals(zone
['records'], recordsCount
)
318 self
.assertEquals(zone
['transfers_full'], fullXFRCount
)
319 self
.assertEquals(zone
['transfers_success'], totalXFRCount
)
322 rpzServer
= RPZServer(rpzServerPort
)
324 class RPZXFRRecursorTest(RPZRecursorTest
):
326 This test makes sure that we correctly update RPZ zones via AXFR then IXFR
330 _lua_config_file
= """
331 -- The first server is a bogus one, to test that we correctly fail over to the second one
332 rpzMaster({'127.0.0.1:9999', '127.0.0.1:%d'}, 'zone.rpz.', { refresh=1 })
333 """ % (rpzServerPort
)
337 _wsPassword
= 'secretpassword'
338 _apiKey
= 'secretapikey'
339 _config_template
= """
340 auth-zones=example=configs/%s/example.zone
343 webserver-address=127.0.0.1
344 webserver-password=%s
346 """ % (_confdir
, _wsPort
, _wsPassword
, _apiKey
)
350 def generateRecursorConfig(cls
, confdir
):
351 authzonepath
= os
.path
.join(confdir
, 'example.zone')
352 with
open(authzonepath
, 'w') as authzone
:
353 authzone
.write("""$ORIGIN example.
355 a 3600 IN A 192.0.2.42
356 b 3600 IN A 192.0.2.42
357 c 3600 IN A 192.0.2.42
358 d 3600 IN A 192.0.2.42
359 e 3600 IN A 192.0.2.42
360 """.format(soa
=cls
._SOA
))
361 super(RPZRecursorTest
, cls
).generateRecursorConfig(confdir
)
363 def waitUntilCorrectSerialIsLoaded(self
, serial
, timeout
=5):
366 rpzServer
.moveToSerial(serial
)
369 while attempts
< timeout
:
370 currentSerial
= rpzServer
.getCurrentSerial()
371 if currentSerial
> serial
:
372 raise AssertionError("Expected serial %d, got %d" % (serial
, currentSerial
))
373 if currentSerial
== serial
:
374 self
._xfrDone
= self
._xfrDone
+ 1
377 attempts
= attempts
+ 1
380 raise AssertionError("Waited %d seconds for the serial to be updated to %d but the serial is still %d" % (timeout
, serial
, currentSerial
))
383 # first zone, only a should be blocked
384 self
.waitUntilCorrectSerialIsLoaded(1)
385 self
.checkRPZStats(1, 1, 1, self
._xfrDone
)
386 self
.checkBlocked('a.example.')
387 self
.checkNotBlocked('b.example.')
388 self
.checkNotBlocked('c.example.')
390 # second zone, a and b should be blocked
391 self
.waitUntilCorrectSerialIsLoaded(2)
392 self
.checkRPZStats(2, 2, 1, self
._xfrDone
)
393 self
.checkBlocked('a.example.')
394 self
.checkBlocked('b.example.')
395 self
.checkNotBlocked('c.example.')
397 # third zone, only b should be blocked
398 self
.waitUntilCorrectSerialIsLoaded(3)
399 self
.checkRPZStats(3, 1, 1, self
._xfrDone
)
400 self
.checkNotBlocked('a.example.')
401 self
.checkBlocked('b.example.')
402 self
.checkNotBlocked('c.example.')
404 # fourth zone, only c should be blocked
405 self
.waitUntilCorrectSerialIsLoaded(4)
406 self
.checkRPZStats(4, 1, 1, self
._xfrDone
)
407 self
.checkNotBlocked('a.example.')
408 self
.checkNotBlocked('b.example.')
409 self
.checkBlocked('c.example.')
411 # fifth zone, we should get a full AXFR this time, and only d should be blocked
412 self
.waitUntilCorrectSerialIsLoaded(5)
413 self
.checkRPZStats(5, 3, 2, self
._xfrDone
)
414 self
.checkNotBlocked('a.example.')
415 self
.checkNotBlocked('b.example.')
416 self
.checkNotBlocked('c.example.')
417 self
.checkBlocked('d.example.')
419 # sixth zone, only e should be blocked, f is a local data record
420 self
.waitUntilCorrectSerialIsLoaded(6)
421 self
.checkRPZStats(6, 2, 2, self
._xfrDone
)
422 self
.checkNotBlocked('a.example.')
423 self
.checkNotBlocked('b.example.')
424 self
.checkNotBlocked('c.example.')
425 self
.checkNotBlocked('d.example.')
426 self
.checkCustom('e.example.', 'A', dns
.rrset
.from_text('e.example.', 0, dns
.rdataclass
.IN
, 'A', '192.0.2.1', '192.0.2.2'))
427 self
.checkCustom('e.example.', 'MX', dns
.rrset
.from_text('e.example.', 0, dns
.rdataclass
.IN
, 'MX', '10 mx.example.'))
428 self
.checkNoData('e.example.', 'AAAA')
429 self
.checkCustom('f.example.', 'A', dns
.rrset
.from_text('f.example.', 0, dns
.rdataclass
.IN
, 'CNAME', 'e.example.'))
431 # seventh zone, e should only have one A
432 self
.waitUntilCorrectSerialIsLoaded(7)
433 self
.checkRPZStats(7, 4, 2, self
._xfrDone
)
434 self
.checkNotBlocked('a.example.')
435 self
.checkNotBlocked('b.example.')
436 self
.checkNotBlocked('c.example.')
437 self
.checkNotBlocked('d.example.')
438 self
.checkCustom('e.example.', 'A', dns
.rrset
.from_text('e.example.', 0, dns
.rdataclass
.IN
, 'A', '192.0.2.2'))
439 self
.checkCustom('e.example.', 'MX', dns
.rrset
.from_text('e.example.', 0, dns
.rdataclass
.IN
, 'MX', '10 mx.example.'))
440 self
.checkNoData('e.example.', 'AAAA')
441 self
.checkCustom('f.example.', 'A', dns
.rrset
.from_text('f.example.', 0, dns
.rdataclass
.IN
, 'CNAME', 'e.example.'))
442 # check that the policy is disabled for AD=1 queries
443 self
.checkNotBlocked('e.example.', True)
444 # check non-custom policies
445 self
.checkTruncated('tc.example.')
446 self
.checkDropped('drop.example.')
448 # eighth zone, all entries should be gone
449 self
.waitUntilCorrectSerialIsLoaded(8)
450 self
.checkRPZStats(8, 0, 3, self
._xfrDone
)
451 self
.checkNotBlocked('a.example.')
452 self
.checkNotBlocked('b.example.')
453 self
.checkNotBlocked('c.example.')
454 self
.checkNotBlocked('d.example.')
455 self
.checkNotBlocked('e.example.')
456 self
.checkNXD('f.example.')
457 self
.checkNXD('tc.example.')
458 self
.checkNXD('drop.example.')
460 class RPZFileRecursorTest(RPZRecursorTest
):
462 This test makes sure that we correctly load RPZ zones from a file
466 _lua_config_file
= """
467 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz." })
469 _config_template
= """
470 auth-zones=example=configs/%s/example.zone
474 def generateRecursorConfig(cls
, confdir
):
475 authzonepath
= os
.path
.join(confdir
, 'example.zone')
476 with
open(authzonepath
, 'w') as authzone
:
477 authzone
.write("""$ORIGIN example.
479 a 3600 IN A 192.0.2.42
480 b 3600 IN A 192.0.2.42
481 c 3600 IN A 192.0.2.42
482 d 3600 IN A 192.0.2.42
483 e 3600 IN A 192.0.2.42
484 z 3600 IN A 192.0.2.42
485 """.format(soa
=cls
._SOA
))
487 rpzFilePath
= os
.path
.join(confdir
, 'zone.rpz')
488 with
open(rpzFilePath
, 'w') as rpzZone
:
489 rpzZone
.write("""$ORIGIN zone.rpz.
491 a.example.zone.rpz. 60 IN A 192.0.2.42
492 a.example.zone.rpz. 60 IN A 192.0.2.43
493 a.example.zone.rpz. 60 IN TXT "some text"
494 drop.example.zone.rpz. 60 IN CNAME rpz-drop.
495 z.example.zone.rpz. 60 IN A 192.0.2.1
496 tc.example.zone.rpz. 60 IN CNAME rpz-tcp-only.
497 """.format(soa
=cls
._SOA
))
498 super(RPZFileRecursorTest
, cls
).generateRecursorConfig(confdir
)
501 self
.checkCustom('a.example.', 'A', dns
.rrset
.from_text('a.example.', 0, dns
.rdataclass
.IN
, 'A', '192.0.2.42', '192.0.2.43'))
502 self
.checkCustom('a.example.', 'TXT', dns
.rrset
.from_text('a.example.', 0, dns
.rdataclass
.IN
, 'TXT', '"some text"'))
503 self
.checkBlocked('z.example.')
504 self
.checkNotBlocked('b.example.')
505 self
.checkNotBlocked('c.example.')
506 self
.checkNotBlocked('d.example.')
507 self
.checkNotBlocked('e.example.')
508 # check that the policy is disabled for AD=1 queries
509 self
.checkNotBlocked('z.example.', True)
510 # check non-custom policies
511 self
.checkTruncated('tc.example.')
512 self
.checkDropped('drop.example.')
514 class RPZFileDefaultPolRecursorTest(RPZRecursorTest
):
516 This test makes sure that we correctly load RPZ zones from a file with a default policy
519 _confdir
= 'RPZFileDefaultPolicy'
520 _lua_config_file
= """
521 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz.", defpol=Policy.NoAction })
523 _config_template
= """
524 auth-zones=example=configs/%s/example.zone
528 def generateRecursorConfig(cls
, confdir
):
529 authzonepath
= os
.path
.join(confdir
, 'example.zone')
530 with
open(authzonepath
, 'w') as authzone
:
531 authzone
.write("""$ORIGIN example.
533 a 3600 IN A 192.0.2.42
534 b 3600 IN A 192.0.2.42
535 c 3600 IN A 192.0.2.42
536 d 3600 IN A 192.0.2.42
537 drop 3600 IN A 192.0.2.42
538 e 3600 IN A 192.0.2.42
539 z 3600 IN A 192.0.2.42
540 """.format(soa
=cls
._SOA
))
542 rpzFilePath
= os
.path
.join(confdir
, 'zone.rpz')
543 with
open(rpzFilePath
, 'w') as rpzZone
:
544 rpzZone
.write("""$ORIGIN zone.rpz.
546 a.example.zone.rpz. 60 IN A 192.0.2.42
547 drop.example.zone.rpz. 60 IN CNAME rpz-drop.
548 z.example.zone.rpz. 60 IN A 192.0.2.1
549 tc.example.zone.rpz. 60 IN CNAME rpz-tcp-only.
550 """.format(soa
=cls
._SOA
))
551 super(RPZFileDefaultPolRecursorTest
, cls
).generateRecursorConfig(confdir
)
554 # local data entries are overridden by default
555 self
.checkCustom('a.example.', 'A', dns
.rrset
.from_text('a.example.', 0, dns
.rdataclass
.IN
, 'A', '192.0.2.42'))
556 self
.checkNoData('a.example.', 'TXT')
557 # will not be blocked because the default policy overrides local data entries by default
558 self
.checkNotBlocked('z.example.')
559 self
.checkNotBlocked('b.example.')
560 self
.checkNotBlocked('c.example.')
561 self
.checkNotBlocked('d.example.')
562 self
.checkNotBlocked('e.example.')
563 # check non-local policies, they should be overridden by the default policy
564 self
.checkNXD('tc.example.', 'A')
565 self
.checkNotBlocked('drop.example.')
567 class RPZFileDefaultPolNotOverrideLocalRecursorTest(RPZRecursorTest
):
569 This test makes sure that we correctly load RPZ zones from a file with a default policy, not overriding local data entries
572 _confdir
= 'RPZFileDefaultPolicyNotOverrideLocal'
573 _lua_config_file
= """
574 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz.", defpol=Policy.NoAction, defpolOverrideLocalData=false })
576 _config_template
= """
577 auth-zones=example=configs/%s/example.zone
581 def generateRecursorConfig(cls
, confdir
):
582 authzonepath
= os
.path
.join(confdir
, 'example.zone')
583 with
open(authzonepath
, 'w') as authzone
:
584 authzone
.write("""$ORIGIN example.
586 a 3600 IN A 192.0.2.42
587 b 3600 IN A 192.0.2.42
588 c 3600 IN A 192.0.2.42
589 d 3600 IN A 192.0.2.42
590 drop 3600 IN A 192.0.2.42
591 e 3600 IN A 192.0.2.42
592 z 3600 IN A 192.0.2.42
593 """.format(soa
=cls
._SOA
))
595 rpzFilePath
= os
.path
.join(confdir
, 'zone.rpz')
596 with
open(rpzFilePath
, 'w') as rpzZone
:
597 rpzZone
.write("""$ORIGIN zone.rpz.
599 a.example.zone.rpz. 60 IN A 192.0.2.42
600 a.example.zone.rpz. 60 IN A 192.0.2.43
601 a.example.zone.rpz. 60 IN TXT "some text"
602 drop.example.zone.rpz. 60 IN CNAME rpz-drop.
603 z.example.zone.rpz. 60 IN A 192.0.2.1
604 tc.example.zone.rpz. 60 IN CNAME rpz-tcp-only.
605 """.format(soa
=cls
._SOA
))
606 super(RPZFileDefaultPolNotOverrideLocalRecursorTest
, cls
).generateRecursorConfig(confdir
)
609 # local data entries will not be overridden by the default polic
610 self
.checkCustom('a.example.', 'A', dns
.rrset
.from_text('a.example.', 0, dns
.rdataclass
.IN
, 'A', '192.0.2.42', '192.0.2.43'))
611 self
.checkCustom('a.example.', 'TXT', dns
.rrset
.from_text('a.example.', 0, dns
.rdataclass
.IN
, 'TXT', '"some text"'))
612 # will be blocked because the default policy does not override local data entries
613 self
.checkBlocked('z.example.')
614 self
.checkNotBlocked('b.example.')
615 self
.checkNotBlocked('c.example.')
616 self
.checkNotBlocked('d.example.')
617 self
.checkNotBlocked('e.example.')
618 # check non-local policies, they should be overridden by the default policy
619 self
.checkNXD('tc.example.', 'A')
620 self
.checkNotBlocked('drop.example.')
622 class RPZSimpleAuthServer(object):
624 def __init__(self
, port
):
625 self
._serverPort
= port
626 listener
= threading
.Thread(name
='RPZ Simple Auth Listener', target
=self
._listener
, args
=[])
627 listener
.setDaemon(True)
630 def _getAnswer(self
, message
):
632 response
= dns
.message
.make_response(message
)
633 response
.flags |
= dns
.flags
.AA
635 dns
.rrset
.from_text('nsip.delegated.example.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.A
, '192.0.2.42')
638 response
.answer
= records
642 sock
= socket
.socket(socket
.AF_INET
, socket
.SOCK_DGRAM
)
644 sock
.bind(("127.0.0.1", self
._serverPort
))
645 except socket
.error
as e
:
646 print("Error binding in the RPZ simple auth listener: %s" % str(e
))
651 data
, addr
= sock
.recvfrom(4096)
652 message
= dns
.message
.from_wire(data
)
653 if len(message
.question
) != 1:
654 print('Invalid query, qdcount is %d' % (len(message
.question
)))
657 answer
= self
._getAnswer
(message
)
659 print('Unable to get a response for %s %d' % (message
.question
[0].name
, message
.question
[0].rdtype
))
662 wire
= answer
.to_wire()
663 sock
.sendto(wire
, addr
)
665 except socket
.error
as e
:
666 print('Error in RPZ simple auth socket: %s' % str(e
))
668 rpzAuthServerPort
= 4260
669 rpzAuthServer
= RPZSimpleAuthServer(rpzAuthServerPort
)
671 class RPZOrderingPrecedenceRecursorTest(RPZRecursorTest
):
673 This test makes sure that the recursor respects the RPZ ordering precedence rules
676 _confdir
= 'RPZOrderingPrecedence'
677 _lua_config_file
= """
678 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz."})
679 rpzFile('configs/%s/zone2.rpz', { policyName="zone2.rpz."})
680 """ % (_confdir
, _confdir
)
681 _config_template
= """
682 auth-zones=example=configs/%s/example.zone
683 forward-zones=delegated.example=127.0.0.1:%d
684 """ % (_confdir
, rpzAuthServerPort
)
687 def generateRecursorConfig(cls
, confdir
):
688 authzonepath
= os
.path
.join(confdir
, 'example.zone')
689 with
open(authzonepath
, 'w') as authzone
:
690 authzone
.write("""$ORIGIN example.
692 sub.test 3600 IN A 192.0.2.42
693 passthru-then-blocked-by-higher 3600 IN A 192.0.2.66
694 passthru-then-blocked-by-same 3600 IN A 192.0.2.66
695 blocked-then-passhtru-by-higher 3600 IN A 192.0.2.100
696 """.format(soa
=cls
._SOA
))
698 rpzFilePath
= os
.path
.join(confdir
, 'zone.rpz')
699 with
open(rpzFilePath
, 'w') as rpzZone
:
700 rpzZone
.write("""$ORIGIN zone.rpz.
702 *.test.example.zone.rpz. 60 IN CNAME rpz-passthru.
703 32.66.2.0.192.rpz-ip.zone.rpz. 60 IN A 192.0.2.1
704 32.100.2.0.192.rpz-ip.zone.rpz. 60 IN CNAME rpz-passthru.
705 passthru-then-blocked-by-same.example.zone.rpz. 60 IN CNAME rpz-passthru.
706 32.1.0.0.127.rpz-nsip.zone.rpz. 60 IN CNAME rpz-passthru.
707 """.format(soa
=cls
._SOA
))
709 rpzFilePath
= os
.path
.join(confdir
, 'zone2.rpz')
710 with
open(rpzFilePath
, 'w') as rpzZone
:
711 rpzZone
.write("""$ORIGIN zone2.rpz.
713 sub.test.example.com.zone2.rpz. 60 IN CNAME .
714 passthru-then-blocked-by-higher.example.zone2.rpz. 60 IN CNAME rpz-passthru.
715 blocked-then-passhtru-by-higher.example.zone2.rpz. 60 IN A 192.0.2.1
716 32.42.2.0.192.rpz-ip 60 IN CNAME .
717 """.format(soa
=cls
._SOA
))
719 super(RPZOrderingPrecedenceRecursorTest
, cls
).generateRecursorConfig(confdir
)
721 def testRPZOrderingForQNameAndWhitelisting(self
):
722 # we should first match on the qname (the wildcard, not on the exact name since
723 # we respect the order of the RPZ zones), see the pass-thru rule
724 # and only process RPZ rules of higher precedence.
725 # The subsequent rule on the content of the A should therefore not trigger a NXDOMAIN.
726 self
.checkNotBlocked('sub.test.example.')
728 def testRPZOrderingWhitelistedThenBlockedByHigher(self
):
729 # we should first match on the qname from the second RPZ zone,
730 # continue the resolution process, and get blocked by the content of the A record
731 # based on the first RPZ zone, whose priority is higher than the second one.
732 self
.checkBlocked('passthru-then-blocked-by-higher.example.')
734 def testRPZOrderingWhitelistedThenBlockedBySame(self
):
735 # we should first match on the qname from the first RPZ zone,
736 # continue the resolution process, and NOT get blocked by the content of the A record
737 # based on the same RPZ zone, since it's not higher.
738 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'))
740 def testRPZOrderBlockedThenWhitelisted(self
):
741 # The qname is first blocked by the second RPZ zone
742 # Then, should the resolution process go on, the A record would be whitelisted
744 # This is what the RPZ specification requires, but we currently decided that we
745 # don't want to leak queries to malicious DNS servers and waste time if the qname is blacklisted.
746 # We might change our opinion at some point, though.
747 self
.checkBlocked('blocked-then-passhtru-by-higher.example.')
749 def testRPZOrderDelegate(self
):
750 # The IP of the NS we are going to contact is whitelisted (passthru) in zone 1,
751 # so even though the record (192.0.2.42) returned by the server is blacklisted
752 # by zone 2, it should not be blocked.
753 # We only test once because after that the answer is cached, so the NS is not contacted
754 # and the whitelist is not applied (yes, NSIP and NSDNAME are brittle).
755 self
.checkNotBlocked('nsip.delegated.example.', singleCheck
=True)
757 class RPZNSIPCustomTest(RPZRecursorTest
):
759 This test makes sure that the recursor handles custom RPZ rules in a NSIP
762 _confdir
= 'RPZNSIPCustom'
763 _lua_config_file
= """
764 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz."})
765 rpzFile('configs/%s/zone2.rpz', { policyName="zone2.rpz."})
766 """ % (_confdir
, _confdir
)
767 _config_template
= """
768 auth-zones=example=configs/%s/example.zone
769 forward-zones=delegated.example=127.0.0.1:%d
770 """ % (_confdir
, rpzAuthServerPort
)
773 def generateRecursorConfig(cls
, confdir
):
774 authzonepath
= os
.path
.join(confdir
, 'example.zone')
775 with
open(authzonepath
, 'w') as authzone
:
776 authzone
.write("""$ORIGIN example.
778 """.format(soa
=cls
._SOA
))
780 rpzFilePath
= os
.path
.join(confdir
, 'zone.rpz')
781 with
open(rpzFilePath
, 'w') as rpzZone
:
782 rpzZone
.write("""$ORIGIN zone.rpz.
784 32.1.0.0.127.rpz-nsip.zone.rpz. 60 IN A 192.0.2.1
785 """.format(soa
=cls
._SOA
))
787 rpzFilePath
= os
.path
.join(confdir
, 'zone2.rpz')
788 with
open(rpzFilePath
, 'w') as rpzZone
:
789 rpzZone
.write("""$ORIGIN zone2.rpz.
791 32.1.2.0.192.rpz-ip 60 IN CNAME .
792 """.format(soa
=cls
._SOA
))
794 super(RPZNSIPCustomTest
, cls
).generateRecursorConfig(confdir
)
796 def testRPZDelegate(self
):
797 # The IP of the NS we are going to contact should result in a custom record (192.0.2.1) from zone 1,
798 # so even though the record (192.0.2.1) returned by the server is blacklisted
799 # by zone 2, it should not be blocked.
800 # We only test once because after that the answer is cached, so the NS is not contacted
801 # and the whitelist is not applied (yes, NSIP and NSDNAME are brittle).
802 self
.checkCustom('nsip.delegated.example.', 'A', dns
.rrset
.from_text('nsip.delegated.example.', 0, dns
.rdataclass
.IN
, 'A', '192.0.2.1'))