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 serve 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 check 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
)
145 elif newSerial
== 11:
146 # IXFR with two deltas, the first one adding a 'g' and the second one removing 'f'
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))
157 # this one has two updates in one
158 newSerial
= newSerial
+ 1
159 self
._targetSerial
= self
._targetSerial
+ 1
161 response
.answer
= records
162 return (newSerial
, response
)
164 def _connectionHandler(self
, conn
):
170 (datalen
,) = struct
.unpack("!H", data
)
171 data
= conn
.recv(datalen
)
175 message
= dns
.message
.from_wire(data
)
176 if len(message
.question
) != 1:
177 print('Invalid RPZ query, qdcount is %d' % (len(message
.question
)))
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
))
182 (serial
, answer
) = self
._getAnswer
(message
)
184 print('Unable to get a response for %s %d' % (message
.question
[0].name
, message
.question
[0].rdtype
))
187 wire
= answer
.to_wire()
188 lenprefix
= struct
.pack("!H", len(wire
))
191 conn
.send(bytes([b
]))
195 self
._currentSerial
= serial
201 sock
= socket
.socket(socket
.AF_INET
, socket
.SOCK_STREAM
)
202 sock
.setsockopt(socket
.SOL_SOCKET
, socket
.SO_REUSEPORT
, 1)
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
))
212 (conn
, _
) = sock
.accept()
213 thread
= threading
.Thread(name
='RPZ Connection Handler',
214 target
=self
._connectionHandler
,
216 thread
.setDaemon(True)
219 except socket
.error
as e
:
220 print('Error in RPZ socket: %s' % str(e
))
223 class RPZRecursorTest(RecursorTest
):
226 _wsPassword
= 'secretpassword'
227 _apiKey
= 'secretapikey'
233 'zones': ['example']},
235 _lua_dns_script_file
= """
238 -- disable the RPZ policy named 'zone.rpz' for AD=1 queries
239 if dq:getDH():getAD() then
240 dq:discardPolicy('zone.rpz.')
246 _config_template
= """
247 auth-zones=example=configs/%s/example.zone
250 webserver-address=127.0.0.1
251 webserver-password=%s
254 """ % (_confdir
, _wsPort
, _wsPassword
, _apiKey
)
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
))
261 for rrset
in msg
.additional
:
262 if rrset
.rdtype
== dns
.rdatatype
.SOA
:
267 raise AssertionError("No SOA record found in the authority section:\n%s" % msg
.to_text())
269 def checkBlocked(self
, name
, shouldBeBlocked
=True, adQuery
=False, singleCheck
=False, soa
=False):
270 query
= dns
.message
.make_query(name
, 'A', want_dnssec
=True)
271 query
.flags |
= dns
.flags
.CD
273 query
.flags |
= dns
.flags
.AD
275 for method
in ("sendUDPQuery", "sendTCPQuery"):
276 sender
= getattr(self
, method
)
278 self
.assertRcodeEqual(res
, dns
.rcode
.NOERROR
)
280 expected
= dns
.rrset
.from_text(name
, 0, dns
.rdataclass
.IN
, 'A', '192.0.2.1')
282 expected
= dns
.rrset
.from_text(name
, 0, dns
.rdataclass
.IN
, 'A', '192.0.2.42')
284 self
.assertRRsetInAnswer(res
, expected
)
286 self
.assertAdditionalHasSOA(res
)
290 def checkNotBlocked(self
, name
, adQuery
=False, singleCheck
=False):
291 self
.checkBlocked(name
, False, adQuery
, singleCheck
)
293 def checkCustom(self
, qname
, qtype
, expected
, soa
=False):
294 query
= dns
.message
.make_query(qname
, qtype
, want_dnssec
=True)
295 query
.flags |
= dns
.flags
.CD
296 for method
in ("sendUDPQuery", "sendTCPQuery"):
297 sender
= getattr(self
, method
)
299 self
.assertRcodeEqual(res
, dns
.rcode
.NOERROR
)
300 self
.assertRRsetInAnswer(res
, expected
)
302 self
.assertAdditionalHasSOA(res
)
304 def checkNoData(self
, qname
, qtype
, soa
=False):
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
)
310 self
.assertRcodeEqual(res
, dns
.rcode
.NOERROR
)
311 self
.assertEqual(len(res
.answer
), 0)
313 self
.assertAdditionalHasSOA(res
)
315 def checkNXD(self
, qname
, qtype
='A'):
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
)
321 self
.assertRcodeEqual(res
, dns
.rcode
.NXDOMAIN
)
322 self
.assertEqual(len(res
.answer
), 0)
323 self
.assertEqual(len(res
.authority
), 1)
325 def checkTruncated(self
, qname
, qtype
='A', soa
=False):
326 query
= dns
.message
.make_query(qname
, qtype
, want_dnssec
=True)
327 query
.flags |
= dns
.flags
.CD
328 res
= self
.sendUDPQuery(query
)
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)
334 self
.assertAdditionalHasSOA(res
)
336 res
= self
.sendTCPQuery(query
)
337 self
.assertRcodeEqual(res
, dns
.rcode
.NXDOMAIN
)
338 self
.assertMessageHasFlags(res
, ['QR', 'RA', 'RD', 'CD'])
339 self
.assertEqual(len(res
.answer
), 0)
340 self
.assertEqual(len(res
.authority
), 1)
341 self
.assertEqual(len(res
.additional
), 0)
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
)
349 self
.assertEqual(res
, None)
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
)
356 self
.assertEqual(r
.status_code
, 200)
357 self
.assertTrue(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
)
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
)
370 rpzServer
= RPZServer(rpzServerPort
)
372 class RPZXFRRecursorTest(RPZRecursorTest
):
374 This test makes sure that we correctly update RPZ zones via AXFR then IXFR
378 _lua_config_file
= """
379 -- The first server is a bogus one, to test that we correctly fail over to the second one
380 rpzMaster({'127.0.0.1:9999', '127.0.0.1:%d'}, 'zone.rpz.', { refresh=1, includeSOA=true})
381 """ % (rpzServerPort
)
385 _wsPassword
= 'secretpassword'
386 _apiKey
= 'secretapikey'
387 _config_template
= """
388 auth-zones=example=configs/%s/example.zone
391 webserver-address=127.0.0.1
392 webserver-password=%s
395 """ % (_confdir
, _wsPort
, _wsPassword
, _apiKey
)
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.
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
)
412 def waitUntilCorrectSerialIsLoaded(self
, serial
, timeout
=5):
415 rpzServer
.moveToSerial(serial
)
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
:
423 self
._xfrDone
= self
._xfrDone
+ 1
426 attempts
= attempts
+ 1
429 raise AssertionError("Waited %d seconds for the serial to be updated to %d but the serial is still %d" % (timeout
, serial
, currentSerial
))
432 self
.waitForTCPSocket("127.0.0.1", self
._wsPort
)
433 # first zone, only a should be blocked
434 self
.waitUntilCorrectSerialIsLoaded(1)
435 self
.checkRPZStats(1, 1, 1, self
._xfrDone
)
436 self
.checkBlocked('a.example.', soa
=True)
437 self
.checkNotBlocked('b.example.')
438 self
.checkNotBlocked('c.example.')
440 # second zone, a and b should be blocked
441 self
.waitUntilCorrectSerialIsLoaded(2)
442 self
.checkRPZStats(2, 2, 1, self
._xfrDone
)
443 self
.checkBlocked('a.example.', soa
=True)
444 self
.checkBlocked('b.example.', soa
=True)
445 self
.checkNotBlocked('c.example.')
447 # third zone, only b should be blocked
448 self
.waitUntilCorrectSerialIsLoaded(3)
449 self
.checkRPZStats(3, 1, 1, self
._xfrDone
)
450 self
.checkNotBlocked('a.example.')
451 self
.checkBlocked('b.example.', soa
=True)
452 self
.checkNotBlocked('c.example.')
454 # fourth zone, only c should be blocked
455 self
.waitUntilCorrectSerialIsLoaded(4)
456 self
.checkRPZStats(4, 1, 1, self
._xfrDone
)
457 self
.checkNotBlocked('a.example.')
458 self
.checkNotBlocked('b.example.')
459 self
.checkBlocked('c.example.', soa
=True)
461 # fifth zone, we should get a full AXFR this time, and only d should be blocked
462 self
.waitUntilCorrectSerialIsLoaded(5)
463 self
.checkRPZStats(5, 3, 2, self
._xfrDone
)
464 self
.checkNotBlocked('a.example.')
465 self
.checkNotBlocked('b.example.')
466 self
.checkNotBlocked('c.example.')
467 self
.checkBlocked('d.example.', soa
=True)
469 # sixth zone, only e should be blocked, f is a local data record
470 self
.waitUntilCorrectSerialIsLoaded(6)
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.')
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)
477 self
.checkCustom('e.example.', 'MX', dns
.rrset
.from_text('e.example.', 0, dns
.rdataclass
.IN
, 'MX', '10 mx.example.'))
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)
481 # seventh zone, e should only have one A
482 self
.waitUntilCorrectSerialIsLoaded(7)
483 self
.checkRPZStats(7, 4, 2, self
._xfrDone
)
484 self
.checkNotBlocked('a.example.')
485 self
.checkNotBlocked('b.example.')
486 self
.checkNotBlocked('c.example.')
487 self
.checkNotBlocked('d.example.')
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)
492 # check that the policy is disabled for AD=1 queries
493 self
.checkNotBlocked('e.example.', True)
494 # check non-custom policies
495 self
.checkTruncated('tc.example.', soa
=True)
496 self
.checkDropped('drop.example.')
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.')
508 self
.checkNXD('drop.example.')
510 # 9th zone is a duplicate, it might get skipped
512 rpzServer
.moveToSerial(9)
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.')
521 self
.checkBlocked('f.example.', soa
=True)
522 self
.checkNXD('tc.example.')
523 self
.checkNXD('drop.example.')
525 # the next update will update the zone twice
526 rpzServer
.moveToSerial(11)
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.')
536 self
.checkBlocked('g.example.', soa
=True)
537 self
.checkNXD('tc.example.')
538 self
.checkNXD('drop.example.')
540 class RPZFileRecursorTest(RPZRecursorTest
):
542 This test makes sure that we correctly load RPZ zones from a file
546 _lua_config_file
= """
547 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz.", includeSOA=true })
549 _config_template
= """
550 auth-zones=example=configs/%s/example.zone
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.
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
))
567 rpzFilePath
= os
.path
.join(confdir
, 'zone.rpz')
568 with
open(rpzFilePath
, 'w') as rpzZone
:
569 rpzZone
.write("""$ORIGIN zone.rpz.
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
)
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"'))
583 self
.checkBlocked('z.example.', soa
=True)
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
591 self
.checkTruncated('tc.example.', soa
=True)
592 self
.checkDropped('drop.example.')
594 class RPZFileDefaultPolRecursorTest(RPZRecursorTest
):
596 This test makes sure that we correctly load RPZ zones from a file with a default policy
599 _confdir
= 'RPZFileDefaultPolicy'
600 _lua_config_file
= """
601 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz.", defpol=Policy.NoAction })
603 _config_template
= """
604 auth-zones=example=configs/%s/example.zone
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.
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
))
622 rpzFilePath
= os
.path
.join(confdir
, 'zone.rpz')
623 with
open(rpzFilePath
, 'w') as rpzZone
:
624 rpzZone
.write("""$ORIGIN zone.rpz.
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
)
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.')
647 class RPZFileDefaultPolNotOverrideLocalRecursorTest(RPZRecursorTest
):
649 This test makes sure that we correctly load RPZ zones from a file with a default policy, not overriding local data entries
652 _confdir
= 'RPZFileDefaultPolicyNotOverrideLocal'
653 _lua_config_file
= """
654 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz.", defpol=Policy.NoAction, defpolOverrideLocalData=false })
656 _config_template
= """
657 auth-zones=example=configs/%s/example.zone
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.
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
))
675 rpzFilePath
= os
.path
.join(confdir
, 'zone.rpz')
676 with
open(rpzFilePath
, 'w') as rpzZone
:
677 rpzZone
.write("""$ORIGIN zone.rpz.
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
)
689 # local data entries will not be overridden by the default policy
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.')
702 class RPZSimpleAuthServer(object):
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)
710 def _getAnswer(self
, message
):
712 response
= dns
.message
.make_response(message
)
713 response
.flags |
= dns
.flags
.AA
715 dns
.rrset
.from_text('nsip.delegated.example.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.A
, '192.0.2.42')
718 response
.answer
= records
722 sock
= socket
.socket(socket
.AF_INET
, socket
.SOCK_DGRAM
)
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
))
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
)))
737 answer
= self
._getAnswer
(message
)
739 print('Unable to get a response for %s %d' % (message
.question
[0].name
, message
.question
[0].rdtype
))
742 wire
= answer
.to_wire()
743 sock
.sendto(wire
, addr
)
745 except socket
.error
as e
:
746 print('Error in RPZ simple auth socket: %s' % str(e
))
748 rpzAuthServerPort
= 4260
749 rpzAuthServer
= RPZSimpleAuthServer(rpzAuthServerPort
)
751 class RPZOrderingPrecedenceRecursorTest(RPZRecursorTest
):
753 This test makes sure that the recursor respects the RPZ ordering precedence rules
756 _confdir
= 'RPZOrderingPrecedence'
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
763 forward-zones=delegated.example=127.0.0.1:%d
764 """ % (_confdir
, rpzAuthServerPort
)
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.
772 sub.test 3600 IN A 192.0.2.42
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
776 """.format(soa
=cls
._SOA
))
778 rpzFilePath
= os
.path
.join(confdir
, 'zone.rpz')
779 with
open(rpzFilePath
, 'w') as rpzZone
:
780 rpzZone
.write("""$ORIGIN zone.rpz.
782 *.test.example.zone.rpz. 60 IN CNAME rpz-passthru.
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.
786 32.1.0.0.127.rpz-nsip.zone.rpz. 60 IN CNAME rpz-passthru.
787 """.format(soa
=cls
._SOA
))
789 rpzFilePath
= os
.path
.join(confdir
, 'zone2.rpz')
790 with
open(rpzFilePath
, 'w') as rpzZone
:
791 rpzZone
.write("""$ORIGIN zone2.rpz.
793 sub.test.example.com.zone2.rpz. 60 IN CNAME .
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
796 32.42.2.0.192.rpz-ip 60 IN CNAME .
797 """.format(soa
=cls
._SOA
))
799 super(RPZOrderingPrecedenceRecursorTest
, cls
).generateRecursorConfig(confdir
)
801 def testRPZOrderingForQNameAndWhitelisting(self
):
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
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.
806 self
.checkNotBlocked('sub.test.example.')
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.')
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'))
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
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.')
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)
837 class RPZNSIPCustomTest(RPZRecursorTest
):
839 This test makes sure that the recursor handles custom RPZ rules in a NSIP
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
)
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.
858 """.format(soa
=cls
._SOA
))
860 rpzFilePath
= os
.path
.join(confdir
, 'zone.rpz')
861 with
open(rpzFilePath
, 'w') as rpzZone
:
862 rpzZone
.write("""$ORIGIN zone.rpz.
864 32.1.0.0.127.rpz-nsip.zone.rpz. 60 IN A 192.0.2.1
865 """.format(soa
=cls
._SOA
))
867 rpzFilePath
= os
.path
.join(confdir
, 'zone2.rpz')
868 with
open(rpzFilePath
, 'w') as rpzZone
:
869 rpzZone
.write("""$ORIGIN zone2.rpz.
871 32.1.2.0.192.rpz-ip 60 IN CNAME .
872 """.format(soa
=cls
._SOA
))
874 super(RPZNSIPCustomTest
, cls
).generateRecursorConfig(confdir
)
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'))
885 class RPZResponseIPCNameChainCustomTest(RPZRecursorTest
):
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.
891 _confdir
= 'RPZResponseIPCNameChainCustom'
892 _lua_config_file
= """
893 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz."})
895 _config_template
= """
896 auth-zones=example=configs/%s/example.zone
897 forward-zones=delegated.example=127.0.0.1:%d
898 """ % (_confdir
, rpzAuthServerPort
)
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.
907 cname IN A 192.0.2.255
908 custom-target IN A 192.0.2.254
909 """.format(soa
=cls
._SOA
))
911 rpzFilePath
= os
.path
.join(confdir
, 'zone.rpz')
912 with
open(rpzFilePath
, 'w') as rpzZone
:
913 rpzZone
.write("""$ORIGIN zone.rpz.
915 cname.example IN CNAME custom-target.example.
916 custom-target.example IN A 192.0.2.253
917 """.format(soa
=cls
._SOA
))
919 super(RPZResponseIPCNameChainCustomTest
, cls
).generateRecursorConfig(confdir
)
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
931 # two times to check the cache
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
)
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'))
944 class RPZCNameChainCustomTest(RPZRecursorTest
):
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).
951 _PREFIX
= os
.environ
['PREFIX']
952 _confdir
= 'RPZCNameChainCustom'
953 _lua_config_file
= """
954 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz."})
956 _config_template
= ""
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.
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
))
969 super(RPZCNameChainCustomTest
, cls
).generateRecursorConfig(confdir
)
971 def testRPZChainNXD(self
):
972 # we should match the A at the end of the CNAME chain and
975 # two times to check the cache
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
)
982 self
.assertRcodeEqual(res
, dns
.rcode
.NXDOMAIN
)
983 self
.assertEqual(len(res
.answer
), 0)
985 def testRPZChainNODATA(self
):
986 # we should match the A at the end of the CNAME chain and
989 # two times to check the cache
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
)
996 self
.assertRcodeEqual(res
, dns
.rcode
.NOERROR
)
997 self
.assertEqual(len(res
.answer
), 0)
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
1003 # two times to check the cache
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
)
1010 self
.assertRcodeEqual(res
, dns
.rcode
.NOERROR
)
1011 # the original CNAME record is signed
1012 self
.assertEqual(len(res
.answer
), 3)
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'))
1016 class RPZFileModByLuaRecursorTest(RPZRecursorTest
):
1018 This test makes sure that we correctly load RPZ zones from a file while being modified by Lua callbacks
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
1030 function nxdomain(dq)
1031 if dq.qname:equal('nxmod.example.') then
1032 dq.appliedPolicy.policyKind = pdns.policykinds.Drop
1039 if dq.qname:equal('nodatamod.example.') then
1040 dq.appliedPolicy.policyKind = pdns.policykinds.Drop
1046 _lua_config_file
= """
1047 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz." })
1049 _config_template
= """
1050 auth-zones=example=configs/%s/example.zone
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.
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
))
1067 rpzFilePath
= os
.path
.join(confdir
, 'zone.rpz')
1068 with
open(rpzFilePath
, 'w') as rpzZone
:
1069 rpzZone
.write("""$ORIGIN zone.rpz.
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
)
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.')