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 # special case for the 9th update, which might get skipped
56 if oldSerial
!= self
._currentSerial
and self
._currentSerial
!= 9:
57 print('Received an IXFR query with an unexpected serial %d, expected %d' % (oldSerial
, self
._currentSerial
))
58 return (None, self
._currentSerial
)
60 newSerial
= self
._targetSerial
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
),
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
)
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
),
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
)
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
)
89 # this one is a bit special, we are answering with a full AXFR
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'),
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.'),
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
)
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'),
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.'),
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
),
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
)
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'),
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.'),
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
)
123 # this one is a bit special too, we are answering with a full AXFR and the new zone is empty
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
)
129 # IXFR inserting a duplicate, we should not crash and skip it
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
)
138 elif newSerial
== 10:
139 # full AXFR to make sure we are removing the duplicate, adding a record, to the that the update was correctly applied
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
)
146 response
.answer
= records
147 return (newSerial
, response
)
149 def _connectionHandler(self
, conn
):
155 (datalen
,) = struct
.unpack("!H", data
)
156 data
= conn
.recv(datalen
)
160 message
= dns
.message
.from_wire(data
)
161 if len(message
.question
) != 1:
162 print('Invalid RPZ query, qdcount is %d' % (len(message
.question
)))
164 if not message
.question
[0].rdtype
in [dns
.rdatatype
.AXFR
, dns
.rdatatype
.IXFR
]:
165 print('Invalid RPZ query, qtype is %d' % (message
.question
.rdtype
))
167 (serial
, answer
) = self
._getAnswer
(message
)
169 print('Unable to get a response for %s %d' % (message
.question
[0].name
, message
.question
[0].rdtype
))
172 wire
= answer
.to_wire()
173 conn
.send(struct
.pack("!H", len(wire
)))
175 self
._currentSerial
= serial
181 sock
= socket
.socket(socket
.AF_INET
, socket
.SOCK_STREAM
)
182 sock
.setsockopt(socket
.SOL_SOCKET
, socket
.SO_REUSEPORT
, 1)
184 sock
.bind(("127.0.0.1", self
._serverPort
))
185 except socket
.error
as e
:
186 print("Error binding in the RPZ listener: %s" % str(e
))
192 (conn
, _
) = sock
.accept()
193 thread
= threading
.Thread(name
='RPZ Connection Handler',
194 target
=self
._connectionHandler
,
196 thread
.setDaemon(True)
199 except socket
.error
as e
:
200 print('Error in RPZ socket: %s' % str(e
))
203 class RPZRecursorTest(RecursorTest
):
206 _wsPassword
= 'secretpassword'
207 _apiKey
= 'secretapikey'
209 _lua_dns_script_file
= """
212 -- disable the RPZ policy named 'zone.rpz' for AD=1 queries
213 if dq:getDH():getAD() then
214 dq:discardPolicy('zone.rpz.')
220 _config_template
= """
221 auth-zones=example=configs/%s/example.zone
224 webserver-address=127.0.0.1
225 webserver-password=%s
228 """ % (_confdir
, _wsPort
, _wsPassword
, _apiKey
)
234 cls
.startResponders()
236 confdir
= os
.path
.join('configs', cls
._confdir
)
237 cls
.createConfigDir(confdir
)
239 cls
.generateRecursorConfig(confdir
)
240 cls
.startRecursor(confdir
, cls
._recursorPort
)
243 def tearDownClass(cls
):
244 cls
.tearDownRecursor()
246 def checkBlocked(self
, name
, shouldBeBlocked
=True, adQuery
=False, singleCheck
=False):
247 query
= dns
.message
.make_query(name
, 'A', want_dnssec
=True)
248 query
.flags |
= dns
.flags
.CD
250 query
.flags |
= dns
.flags
.AD
252 for method
in ("sendUDPQuery", "sendTCPQuery"):
253 sender
= getattr(self
, method
)
255 self
.assertRcodeEqual(res
, dns
.rcode
.NOERROR
)
257 expected
= dns
.rrset
.from_text(name
, 0, dns
.rdataclass
.IN
, 'A', '192.0.2.1')
259 expected
= dns
.rrset
.from_text(name
, 0, dns
.rdataclass
.IN
, 'A', '192.0.2.42')
261 self
.assertRRsetInAnswer(res
, expected
)
265 def checkNotBlocked(self
, name
, adQuery
=False, singleCheck
=False):
266 self
.checkBlocked(name
, False, adQuery
, singleCheck
)
268 def checkCustom(self
, qname
, qtype
, expected
):
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
.NOERROR
)
275 self
.assertRRsetInAnswer(res
, expected
)
277 def checkNoData(self
, qname
, qtype
):
278 query
= dns
.message
.make_query(qname
, qtype
, want_dnssec
=True)
279 query
.flags |
= dns
.flags
.CD
280 for method
in ("sendUDPQuery", "sendTCPQuery"):
281 sender
= getattr(self
, method
)
283 self
.assertRcodeEqual(res
, dns
.rcode
.NOERROR
)
284 self
.assertEqual(len(res
.answer
), 0)
286 def checkNXD(self
, qname
, qtype
='A'):
287 query
= dns
.message
.make_query(qname
, qtype
, want_dnssec
=True)
288 query
.flags |
= dns
.flags
.CD
289 for method
in ("sendUDPQuery", "sendTCPQuery"):
290 sender
= getattr(self
, method
)
292 self
.assertRcodeEqual(res
, dns
.rcode
.NXDOMAIN
)
293 self
.assertEqual(len(res
.answer
), 0)
294 self
.assertEqual(len(res
.authority
), 1)
296 def checkTruncated(self
, qname
, qtype
='A'):
297 query
= dns
.message
.make_query(qname
, qtype
, want_dnssec
=True)
298 query
.flags |
= dns
.flags
.CD
299 res
= self
.sendUDPQuery(query
)
300 self
.assertRcodeEqual(res
, dns
.rcode
.NOERROR
)
301 self
.assertMessageHasFlags(res
, ['QR', 'RA', 'RD', 'CD', 'TC'])
302 self
.assertEqual(len(res
.answer
), 0)
303 self
.assertEqual(len(res
.authority
), 0)
304 self
.assertEqual(len(res
.additional
), 0)
306 res
= self
.sendTCPQuery(query
)
307 self
.assertRcodeEqual(res
, dns
.rcode
.NXDOMAIN
)
308 self
.assertMessageHasFlags(res
, ['QR', 'RA', 'RD', 'CD'])
309 self
.assertEqual(len(res
.answer
), 0)
310 self
.assertEqual(len(res
.authority
), 1)
311 self
.assertEqual(len(res
.additional
), 0)
313 def checkDropped(self
, qname
, qtype
='A'):
314 query
= dns
.message
.make_query(qname
, qtype
, want_dnssec
=True)
315 query
.flags |
= dns
.flags
.CD
316 for method
in ("sendUDPQuery", "sendTCPQuery"):
317 sender
= getattr(self
, method
)
319 self
.assertEqual(res
, None)
321 def checkRPZStats(self
, serial
, recordsCount
, fullXFRCount
, totalXFRCount
):
322 headers
= {'x-api-key': self
._apiKey
}
323 url
= 'http://127.0.0.1:' + str(self
._wsPort
) + '/api/v1/servers/localhost/rpzstatistics'
324 r
= requests
.get(url
, headers
=headers
, timeout
=self
._wsTimeout
)
326 self
.assertEquals(r
.status_code
, 200)
327 self
.assertTrue(r
.json())
329 self
.assertIn('zone.rpz.', content
)
330 zone
= content
['zone.rpz.']
331 for key
in ['last_update', 'records', 'serial', 'transfers_failed', 'transfers_full', 'transfers_success']:
332 self
.assertIn(key
, zone
)
334 self
.assertEquals(zone
['serial'], serial
)
335 self
.assertEquals(zone
['records'], recordsCount
)
336 self
.assertEquals(zone
['transfers_full'], fullXFRCount
)
337 self
.assertEquals(zone
['transfers_success'], totalXFRCount
)
340 rpzServer
= RPZServer(rpzServerPort
)
342 class RPZXFRRecursorTest(RPZRecursorTest
):
344 This test makes sure that we correctly update RPZ zones via AXFR then IXFR
348 _lua_config_file
= """
349 -- The first server is a bogus one, to test that we correctly fail over to the second one
350 rpzMaster({'127.0.0.1:9999', '127.0.0.1:%d'}, 'zone.rpz.', { refresh=1 })
351 """ % (rpzServerPort
)
355 _wsPassword
= 'secretpassword'
356 _apiKey
= 'secretapikey'
357 _config_template
= """
358 auth-zones=example=configs/%s/example.zone
361 webserver-address=127.0.0.1
362 webserver-password=%s
364 """ % (_confdir
, _wsPort
, _wsPassword
, _apiKey
)
368 def generateRecursorConfig(cls
, confdir
):
369 authzonepath
= os
.path
.join(confdir
, 'example.zone')
370 with
open(authzonepath
, 'w') as authzone
:
371 authzone
.write("""$ORIGIN example.
373 a 3600 IN A 192.0.2.42
374 b 3600 IN A 192.0.2.42
375 c 3600 IN A 192.0.2.42
376 d 3600 IN A 192.0.2.42
377 e 3600 IN A 192.0.2.42
378 """.format(soa
=cls
._SOA
))
379 super(RPZRecursorTest
, cls
).generateRecursorConfig(confdir
)
381 def waitUntilCorrectSerialIsLoaded(self
, serial
, timeout
=5):
384 rpzServer
.moveToSerial(serial
)
387 while attempts
< timeout
:
388 currentSerial
= rpzServer
.getCurrentSerial()
389 if currentSerial
> serial
:
390 raise AssertionError("Expected serial %d, got %d" % (serial
, currentSerial
))
391 if currentSerial
== serial
:
392 self
._xfrDone
= self
._xfrDone
+ 1
395 attempts
= attempts
+ 1
398 raise AssertionError("Waited %d seconds for the serial to be updated to %d but the serial is still %d" % (timeout
, serial
, currentSerial
))
401 # first zone, only a should be blocked
402 self
.waitUntilCorrectSerialIsLoaded(1)
403 self
.checkRPZStats(1, 1, 1, self
._xfrDone
)
404 self
.checkBlocked('a.example.')
405 self
.checkNotBlocked('b.example.')
406 self
.checkNotBlocked('c.example.')
408 # second zone, a and b should be blocked
409 self
.waitUntilCorrectSerialIsLoaded(2)
410 self
.checkRPZStats(2, 2, 1, self
._xfrDone
)
411 self
.checkBlocked('a.example.')
412 self
.checkBlocked('b.example.')
413 self
.checkNotBlocked('c.example.')
415 # third zone, only b should be blocked
416 self
.waitUntilCorrectSerialIsLoaded(3)
417 self
.checkRPZStats(3, 1, 1, self
._xfrDone
)
418 self
.checkNotBlocked('a.example.')
419 self
.checkBlocked('b.example.')
420 self
.checkNotBlocked('c.example.')
422 # fourth zone, only c should be blocked
423 self
.waitUntilCorrectSerialIsLoaded(4)
424 self
.checkRPZStats(4, 1, 1, self
._xfrDone
)
425 self
.checkNotBlocked('a.example.')
426 self
.checkNotBlocked('b.example.')
427 self
.checkBlocked('c.example.')
429 # fifth zone, we should get a full AXFR this time, and only d should be blocked
430 self
.waitUntilCorrectSerialIsLoaded(5)
431 self
.checkRPZStats(5, 3, 2, self
._xfrDone
)
432 self
.checkNotBlocked('a.example.')
433 self
.checkNotBlocked('b.example.')
434 self
.checkNotBlocked('c.example.')
435 self
.checkBlocked('d.example.')
437 # sixth zone, only e should be blocked, f is a local data record
438 self
.waitUntilCorrectSerialIsLoaded(6)
439 self
.checkRPZStats(6, 2, 2, self
._xfrDone
)
440 self
.checkNotBlocked('a.example.')
441 self
.checkNotBlocked('b.example.')
442 self
.checkNotBlocked('c.example.')
443 self
.checkNotBlocked('d.example.')
444 self
.checkCustom('e.example.', 'A', dns
.rrset
.from_text('e.example.', 0, dns
.rdataclass
.IN
, 'A', '192.0.2.1', '192.0.2.2'))
445 self
.checkCustom('e.example.', 'MX', dns
.rrset
.from_text('e.example.', 0, dns
.rdataclass
.IN
, 'MX', '10 mx.example.'))
446 self
.checkNoData('e.example.', 'AAAA')
447 self
.checkCustom('f.example.', 'A', dns
.rrset
.from_text('f.example.', 0, dns
.rdataclass
.IN
, 'CNAME', 'e.example.'))
449 # seventh zone, e should only have one A
450 self
.waitUntilCorrectSerialIsLoaded(7)
451 self
.checkRPZStats(7, 4, 2, self
._xfrDone
)
452 self
.checkNotBlocked('a.example.')
453 self
.checkNotBlocked('b.example.')
454 self
.checkNotBlocked('c.example.')
455 self
.checkNotBlocked('d.example.')
456 self
.checkCustom('e.example.', 'A', dns
.rrset
.from_text('e.example.', 0, dns
.rdataclass
.IN
, 'A', '192.0.2.2'))
457 self
.checkCustom('e.example.', 'MX', dns
.rrset
.from_text('e.example.', 0, dns
.rdataclass
.IN
, 'MX', '10 mx.example.'))
458 self
.checkNoData('e.example.', 'AAAA')
459 self
.checkCustom('f.example.', 'A', dns
.rrset
.from_text('f.example.', 0, dns
.rdataclass
.IN
, 'CNAME', 'e.example.'))
460 # check that the policy is disabled for AD=1 queries
461 self
.checkNotBlocked('e.example.', True)
462 # check non-custom policies
463 self
.checkTruncated('tc.example.')
464 self
.checkDropped('drop.example.')
466 # eighth zone, all entries should be gone
467 self
.waitUntilCorrectSerialIsLoaded(8)
468 self
.checkRPZStats(8, 0, 3, self
._xfrDone
)
469 self
.checkNotBlocked('a.example.')
470 self
.checkNotBlocked('b.example.')
471 self
.checkNotBlocked('c.example.')
472 self
.checkNotBlocked('d.example.')
473 self
.checkNotBlocked('e.example.')
474 self
.checkNXD('f.example.')
475 self
.checkNXD('tc.example.')
476 self
.checkNXD('drop.example.')
478 # 9th zone is a duplicate, it might get skipped
480 rpzServer
.moveToSerial(9)
482 self
.waitUntilCorrectSerialIsLoaded(10)
483 self
.checkRPZStats(10, 1, 4, self
._xfrDone
)
484 self
.checkNotBlocked('a.example.')
485 self
.checkNotBlocked('b.example.')
486 self
.checkNotBlocked('c.example.')
487 self
.checkNotBlocked('d.example.')
488 self
.checkNotBlocked('e.example.')
489 self
.checkBlocked('f.example.')
490 self
.checkNXD('tc.example.')
491 self
.checkNXD('drop.example.')
493 class RPZFileRecursorTest(RPZRecursorTest
):
495 This test makes sure that we correctly load RPZ zones from a file
499 _lua_config_file
= """
500 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz." })
502 _config_template
= """
503 auth-zones=example=configs/%s/example.zone
507 def generateRecursorConfig(cls
, confdir
):
508 authzonepath
= os
.path
.join(confdir
, 'example.zone')
509 with
open(authzonepath
, 'w') as authzone
:
510 authzone
.write("""$ORIGIN example.
512 a 3600 IN A 192.0.2.42
513 b 3600 IN A 192.0.2.42
514 c 3600 IN A 192.0.2.42
515 d 3600 IN A 192.0.2.42
516 e 3600 IN A 192.0.2.42
517 z 3600 IN A 192.0.2.42
518 """.format(soa
=cls
._SOA
))
520 rpzFilePath
= os
.path
.join(confdir
, 'zone.rpz')
521 with
open(rpzFilePath
, 'w') as rpzZone
:
522 rpzZone
.write("""$ORIGIN zone.rpz.
524 a.example.zone.rpz. 60 IN A 192.0.2.42
525 a.example.zone.rpz. 60 IN A 192.0.2.43
526 a.example.zone.rpz. 60 IN TXT "some text"
527 drop.example.zone.rpz. 60 IN CNAME rpz-drop.
528 z.example.zone.rpz. 60 IN A 192.0.2.1
529 tc.example.zone.rpz. 60 IN CNAME rpz-tcp-only.
530 """.format(soa
=cls
._SOA
))
531 super(RPZFileRecursorTest
, cls
).generateRecursorConfig(confdir
)
534 self
.checkCustom('a.example.', 'A', dns
.rrset
.from_text('a.example.', 0, dns
.rdataclass
.IN
, 'A', '192.0.2.42', '192.0.2.43'))
535 self
.checkCustom('a.example.', 'TXT', dns
.rrset
.from_text('a.example.', 0, dns
.rdataclass
.IN
, 'TXT', '"some text"'))
536 self
.checkBlocked('z.example.')
537 self
.checkNotBlocked('b.example.')
538 self
.checkNotBlocked('c.example.')
539 self
.checkNotBlocked('d.example.')
540 self
.checkNotBlocked('e.example.')
541 # check that the policy is disabled for AD=1 queries
542 self
.checkNotBlocked('z.example.', True)
543 # check non-custom policies
544 self
.checkTruncated('tc.example.')
545 self
.checkDropped('drop.example.')
547 class RPZFileDefaultPolRecursorTest(RPZRecursorTest
):
549 This test makes sure that we correctly load RPZ zones from a file with a default policy
552 _confdir
= 'RPZFileDefaultPolicy'
553 _lua_config_file
= """
554 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz.", defpol=Policy.NoAction })
556 _config_template
= """
557 auth-zones=example=configs/%s/example.zone
561 def generateRecursorConfig(cls
, confdir
):
562 authzonepath
= os
.path
.join(confdir
, 'example.zone')
563 with
open(authzonepath
, 'w') as authzone
:
564 authzone
.write("""$ORIGIN example.
566 a 3600 IN A 192.0.2.42
567 b 3600 IN A 192.0.2.42
568 c 3600 IN A 192.0.2.42
569 d 3600 IN A 192.0.2.42
570 drop 3600 IN A 192.0.2.42
571 e 3600 IN A 192.0.2.42
572 z 3600 IN A 192.0.2.42
573 """.format(soa
=cls
._SOA
))
575 rpzFilePath
= os
.path
.join(confdir
, 'zone.rpz')
576 with
open(rpzFilePath
, 'w') as rpzZone
:
577 rpzZone
.write("""$ORIGIN zone.rpz.
579 a.example.zone.rpz. 60 IN A 192.0.2.42
580 drop.example.zone.rpz. 60 IN CNAME rpz-drop.
581 z.example.zone.rpz. 60 IN A 192.0.2.1
582 tc.example.zone.rpz. 60 IN CNAME rpz-tcp-only.
583 """.format(soa
=cls
._SOA
))
584 super(RPZFileDefaultPolRecursorTest
, cls
).generateRecursorConfig(confdir
)
587 # local data entries are overridden by default
588 self
.checkCustom('a.example.', 'A', dns
.rrset
.from_text('a.example.', 0, dns
.rdataclass
.IN
, 'A', '192.0.2.42'))
589 self
.checkNoData('a.example.', 'TXT')
590 # will not be blocked because the default policy overrides local data entries by default
591 self
.checkNotBlocked('z.example.')
592 self
.checkNotBlocked('b.example.')
593 self
.checkNotBlocked('c.example.')
594 self
.checkNotBlocked('d.example.')
595 self
.checkNotBlocked('e.example.')
596 # check non-local policies, they should be overridden by the default policy
597 self
.checkNXD('tc.example.', 'A')
598 self
.checkNotBlocked('drop.example.')
600 class RPZFileDefaultPolNotOverrideLocalRecursorTest(RPZRecursorTest
):
602 This test makes sure that we correctly load RPZ zones from a file with a default policy, not overriding local data entries
605 _confdir
= 'RPZFileDefaultPolicyNotOverrideLocal'
606 _lua_config_file
= """
607 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz.", defpol=Policy.NoAction, defpolOverrideLocalData=false })
609 _config_template
= """
610 auth-zones=example=configs/%s/example.zone
614 def generateRecursorConfig(cls
, confdir
):
615 authzonepath
= os
.path
.join(confdir
, 'example.zone')
616 with
open(authzonepath
, 'w') as authzone
:
617 authzone
.write("""$ORIGIN example.
619 a 3600 IN A 192.0.2.42
620 b 3600 IN A 192.0.2.42
621 c 3600 IN A 192.0.2.42
622 d 3600 IN A 192.0.2.42
623 drop 3600 IN A 192.0.2.42
624 e 3600 IN A 192.0.2.42
625 z 3600 IN A 192.0.2.42
626 """.format(soa
=cls
._SOA
))
628 rpzFilePath
= os
.path
.join(confdir
, 'zone.rpz')
629 with
open(rpzFilePath
, 'w') as rpzZone
:
630 rpzZone
.write("""$ORIGIN zone.rpz.
632 a.example.zone.rpz. 60 IN A 192.0.2.42
633 a.example.zone.rpz. 60 IN A 192.0.2.43
634 a.example.zone.rpz. 60 IN TXT "some text"
635 drop.example.zone.rpz. 60 IN CNAME rpz-drop.
636 z.example.zone.rpz. 60 IN A 192.0.2.1
637 tc.example.zone.rpz. 60 IN CNAME rpz-tcp-only.
638 """.format(soa
=cls
._SOA
))
639 super(RPZFileDefaultPolNotOverrideLocalRecursorTest
, cls
).generateRecursorConfig(confdir
)
642 # local data entries will not be overridden by the default policy
643 self
.checkCustom('a.example.', 'A', dns
.rrset
.from_text('a.example.', 0, dns
.rdataclass
.IN
, 'A', '192.0.2.42', '192.0.2.43'))
644 self
.checkCustom('a.example.', 'TXT', dns
.rrset
.from_text('a.example.', 0, dns
.rdataclass
.IN
, 'TXT', '"some text"'))
645 # will be blocked because the default policy does not override local data entries
646 self
.checkBlocked('z.example.')
647 self
.checkNotBlocked('b.example.')
648 self
.checkNotBlocked('c.example.')
649 self
.checkNotBlocked('d.example.')
650 self
.checkNotBlocked('e.example.')
651 # check non-local policies, they should be overridden by the default policy
652 self
.checkNXD('tc.example.', 'A')
653 self
.checkNotBlocked('drop.example.')
655 class RPZSimpleAuthServer(object):
657 def __init__(self
, port
):
658 self
._serverPort
= port
659 listener
= threading
.Thread(name
='RPZ Simple Auth Listener', target
=self
._listener
, args
=[])
660 listener
.setDaemon(True)
663 def _getAnswer(self
, message
):
665 response
= dns
.message
.make_response(message
)
666 response
.flags |
= dns
.flags
.AA
668 dns
.rrset
.from_text('nsip.delegated.example.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.A
, '192.0.2.42')
671 response
.answer
= records
675 sock
= socket
.socket(socket
.AF_INET
, socket
.SOCK_DGRAM
)
677 sock
.bind(("127.0.0.1", self
._serverPort
))
678 except socket
.error
as e
:
679 print("Error binding in the RPZ simple auth listener: %s" % str(e
))
684 data
, addr
= sock
.recvfrom(4096)
685 message
= dns
.message
.from_wire(data
)
686 if len(message
.question
) != 1:
687 print('Invalid query, qdcount is %d' % (len(message
.question
)))
690 answer
= self
._getAnswer
(message
)
692 print('Unable to get a response for %s %d' % (message
.question
[0].name
, message
.question
[0].rdtype
))
695 wire
= answer
.to_wire()
696 sock
.sendto(wire
, addr
)
698 except socket
.error
as e
:
699 print('Error in RPZ simple auth socket: %s' % str(e
))
701 rpzAuthServerPort
= 4260
702 rpzAuthServer
= RPZSimpleAuthServer(rpzAuthServerPort
)
704 class RPZOrderingPrecedenceRecursorTest(RPZRecursorTest
):
706 This test makes sure that the recursor respects the RPZ ordering precedence rules
709 _confdir
= 'RPZOrderingPrecedence'
710 _lua_config_file
= """
711 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz."})
712 rpzFile('configs/%s/zone2.rpz', { policyName="zone2.rpz."})
713 """ % (_confdir
, _confdir
)
714 _config_template
= """
715 auth-zones=example=configs/%s/example.zone
716 forward-zones=delegated.example=127.0.0.1:%d
717 """ % (_confdir
, rpzAuthServerPort
)
720 def generateRecursorConfig(cls
, confdir
):
721 authzonepath
= os
.path
.join(confdir
, 'example.zone')
722 with
open(authzonepath
, 'w') as authzone
:
723 authzone
.write("""$ORIGIN example.
725 sub.test 3600 IN A 192.0.2.42
726 passthru-then-blocked-by-higher 3600 IN A 192.0.2.66
727 passthru-then-blocked-by-same 3600 IN A 192.0.2.66
728 blocked-then-passhtru-by-higher 3600 IN A 192.0.2.100
729 """.format(soa
=cls
._SOA
))
731 rpzFilePath
= os
.path
.join(confdir
, 'zone.rpz')
732 with
open(rpzFilePath
, 'w') as rpzZone
:
733 rpzZone
.write("""$ORIGIN zone.rpz.
735 *.test.example.zone.rpz. 60 IN CNAME rpz-passthru.
736 32.66.2.0.192.rpz-ip.zone.rpz. 60 IN A 192.0.2.1
737 32.100.2.0.192.rpz-ip.zone.rpz. 60 IN CNAME rpz-passthru.
738 passthru-then-blocked-by-same.example.zone.rpz. 60 IN CNAME rpz-passthru.
739 32.1.0.0.127.rpz-nsip.zone.rpz. 60 IN CNAME rpz-passthru.
740 """.format(soa
=cls
._SOA
))
742 rpzFilePath
= os
.path
.join(confdir
, 'zone2.rpz')
743 with
open(rpzFilePath
, 'w') as rpzZone
:
744 rpzZone
.write("""$ORIGIN zone2.rpz.
746 sub.test.example.com.zone2.rpz. 60 IN CNAME .
747 passthru-then-blocked-by-higher.example.zone2.rpz. 60 IN CNAME rpz-passthru.
748 blocked-then-passhtru-by-higher.example.zone2.rpz. 60 IN A 192.0.2.1
749 32.42.2.0.192.rpz-ip 60 IN CNAME .
750 """.format(soa
=cls
._SOA
))
752 super(RPZOrderingPrecedenceRecursorTest
, cls
).generateRecursorConfig(confdir
)
754 def testRPZOrderingForQNameAndWhitelisting(self
):
755 # we should first match on the qname (the wildcard, not on the exact name since
756 # we respect the order of the RPZ zones), see the pass-thru rule
757 # and only process RPZ rules of higher precedence.
758 # The subsequent rule on the content of the A should therefore not trigger a NXDOMAIN.
759 self
.checkNotBlocked('sub.test.example.')
761 def testRPZOrderingWhitelistedThenBlockedByHigher(self
):
762 # we should first match on the qname from the second RPZ zone,
763 # continue the resolution process, and get blocked by the content of the A record
764 # based on the first RPZ zone, whose priority is higher than the second one.
765 self
.checkBlocked('passthru-then-blocked-by-higher.example.')
767 def testRPZOrderingWhitelistedThenBlockedBySame(self
):
768 # we should first match on the qname from the first RPZ zone,
769 # continue the resolution process, and NOT get blocked by the content of the A record
770 # based on the same RPZ zone, since it's not higher.
771 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'))
773 def testRPZOrderBlockedThenWhitelisted(self
):
774 # The qname is first blocked by the second RPZ zone
775 # Then, should the resolution process go on, the A record would be whitelisted
777 # This is what the RPZ specification requires, but we currently decided that we
778 # don't want to leak queries to malicious DNS servers and waste time if the qname is blacklisted.
779 # We might change our opinion at some point, though.
780 self
.checkBlocked('blocked-then-passhtru-by-higher.example.')
782 def testRPZOrderDelegate(self
):
783 # The IP of the NS we are going to contact is whitelisted (passthru) in zone 1,
784 # so even though the record (192.0.2.42) returned by the server is blacklisted
785 # by zone 2, it should not be blocked.
786 # We only test once because after that the answer is cached, so the NS is not contacted
787 # and the whitelist is not applied (yes, NSIP and NSDNAME are brittle).
788 self
.checkNotBlocked('nsip.delegated.example.', singleCheck
=True)
790 class RPZNSIPCustomTest(RPZRecursorTest
):
792 This test makes sure that the recursor handles custom RPZ rules in a NSIP
795 _confdir
= 'RPZNSIPCustom'
796 _lua_config_file
= """
797 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz."})
798 rpzFile('configs/%s/zone2.rpz', { policyName="zone2.rpz."})
799 """ % (_confdir
, _confdir
)
800 _config_template
= """
801 auth-zones=example=configs/%s/example.zone
802 forward-zones=delegated.example=127.0.0.1:%d
803 """ % (_confdir
, rpzAuthServerPort
)
806 def generateRecursorConfig(cls
, confdir
):
807 authzonepath
= os
.path
.join(confdir
, 'example.zone')
808 with
open(authzonepath
, 'w') as authzone
:
809 authzone
.write("""$ORIGIN example.
811 """.format(soa
=cls
._SOA
))
813 rpzFilePath
= os
.path
.join(confdir
, 'zone.rpz')
814 with
open(rpzFilePath
, 'w') as rpzZone
:
815 rpzZone
.write("""$ORIGIN zone.rpz.
817 32.1.0.0.127.rpz-nsip.zone.rpz. 60 IN A 192.0.2.1
818 """.format(soa
=cls
._SOA
))
820 rpzFilePath
= os
.path
.join(confdir
, 'zone2.rpz')
821 with
open(rpzFilePath
, 'w') as rpzZone
:
822 rpzZone
.write("""$ORIGIN zone2.rpz.
824 32.1.2.0.192.rpz-ip 60 IN CNAME .
825 """.format(soa
=cls
._SOA
))
827 super(RPZNSIPCustomTest
, cls
).generateRecursorConfig(confdir
)
829 def testRPZDelegate(self
):
830 # The IP of the NS we are going to contact should result in a custom record (192.0.2.1) from zone 1,
831 # so even though the record (192.0.2.1) 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
.checkCustom('nsip.delegated.example.', 'A', dns
.rrset
.from_text('nsip.delegated.example.', 0, dns
.rdataclass
.IN
, 'A', '192.0.2.1'))