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
.daemon
= 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
,
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 sendNotify(self
):
257 notify
= dns
.message
.make_query('zone.rpz', 'SOA', want_dnssec
=False)
258 notify
.set_opcode(4) # notify
259 res
= self
.sendUDPQuery(notify
)
260 self
.assertRcodeEqual(res
, dns
.rcode
.NOERROR
)
261 self
.assertEqual(res
.opcode(), 4)
262 self
.assertEqual(res
.question
[0].to_text(), 'zone.rpz. IN SOA')
264 def assertAdditionalHasSOA(self
, msg
):
265 if not isinstance(msg
, dns
.message
.Message
):
266 raise TypeError("msg is not a dns.message.Message but a %s" % type(msg
))
269 for rrset
in msg
.additional
:
270 if rrset
.rdtype
== dns
.rdatatype
.SOA
:
275 raise AssertionError("No SOA record found in the authority section:\n%s" % msg
.to_text())
277 def checkBlocked(self
, name
, shouldBeBlocked
=True, adQuery
=False, singleCheck
=False, soa
=False):
278 query
= dns
.message
.make_query(name
, 'A', want_dnssec
=True)
279 query
.flags |
= dns
.flags
.CD
281 query
.flags |
= dns
.flags
.AD
283 for method
in ("sendUDPQuery", "sendTCPQuery"):
284 sender
= getattr(self
, method
)
286 self
.assertRcodeEqual(res
, dns
.rcode
.NOERROR
)
288 expected
= dns
.rrset
.from_text(name
, 0, dns
.rdataclass
.IN
, 'A', '192.0.2.1')
290 expected
= dns
.rrset
.from_text(name
, 0, dns
.rdataclass
.IN
, 'A', '192.0.2.42')
292 self
.assertRRsetInAnswer(res
, expected
)
294 self
.assertAdditionalHasSOA(res
)
298 def checkNotBlocked(self
, name
, adQuery
=False, singleCheck
=False):
299 self
.checkBlocked(name
, False, adQuery
, singleCheck
)
301 def checkCustom(self
, qname
, qtype
, expected
, soa
=False):
302 query
= dns
.message
.make_query(qname
, qtype
, want_dnssec
=True)
303 query
.flags |
= dns
.flags
.CD
304 for method
in ("sendUDPQuery", "sendTCPQuery"):
305 sender
= getattr(self
, method
)
307 self
.assertRcodeEqual(res
, dns
.rcode
.NOERROR
)
308 self
.assertRRsetInAnswer(res
, expected
)
310 self
.assertAdditionalHasSOA(res
)
312 def checkNoData(self
, qname
, qtype
, soa
=False):
313 query
= dns
.message
.make_query(qname
, qtype
, want_dnssec
=True)
314 query
.flags |
= dns
.flags
.CD
315 for method
in ("sendUDPQuery", "sendTCPQuery"):
316 sender
= getattr(self
, method
)
318 self
.assertRcodeEqual(res
, dns
.rcode
.NOERROR
)
319 self
.assertEqual(len(res
.answer
), 0)
321 self
.assertAdditionalHasSOA(res
)
323 def checkNXD(self
, qname
, qtype
='A'):
324 query
= dns
.message
.make_query(qname
, qtype
, want_dnssec
=True)
325 query
.flags |
= dns
.flags
.CD
326 for method
in ("sendUDPQuery", "sendTCPQuery"):
327 sender
= getattr(self
, method
)
329 self
.assertRcodeEqual(res
, dns
.rcode
.NXDOMAIN
)
330 self
.assertEqual(len(res
.answer
), 0)
331 self
.assertEqual(len(res
.authority
), 1)
333 def checkTruncated(self
, qname
, qtype
='A', soa
=False):
334 query
= dns
.message
.make_query(qname
, qtype
, want_dnssec
=True)
335 query
.flags |
= dns
.flags
.CD
336 res
= self
.sendUDPQuery(query
)
337 self
.assertRcodeEqual(res
, dns
.rcode
.NOERROR
)
338 self
.assertMessageHasFlags(res
, ['QR', 'RA', 'RD', 'CD', 'TC'])
339 self
.assertEqual(len(res
.answer
), 0)
340 self
.assertEqual(len(res
.authority
), 0)
342 self
.assertAdditionalHasSOA(res
)
344 res
= self
.sendTCPQuery(query
)
345 self
.assertRcodeEqual(res
, dns
.rcode
.NXDOMAIN
)
346 self
.assertMessageHasFlags(res
, ['QR', 'RA', 'RD', 'CD'])
347 self
.assertEqual(len(res
.answer
), 0)
348 self
.assertEqual(len(res
.authority
), 1)
349 self
.assertEqual(len(res
.additional
), 0)
351 def checkDropped(self
, qname
, qtype
='A'):
352 query
= dns
.message
.make_query(qname
, qtype
, want_dnssec
=True)
353 query
.flags |
= dns
.flags
.CD
354 for method
in ("sendUDPQuery", "sendTCPQuery"):
355 sender
= getattr(self
, method
)
357 self
.assertEqual(res
, None)
359 def checkRPZStats(self
, serial
, recordsCount
, fullXFRCount
, totalXFRCount
):
360 headers
= {'x-api-key': self
._apiKey
}
361 url
= 'http://127.0.0.1:' + str(self
._wsPort
) + '/api/v1/servers/localhost/rpzstatistics'
362 r
= requests
.get(url
, headers
=headers
, timeout
=self
._wsTimeout
)
364 self
.assertEqual(r
.status_code
, 200)
365 self
.assertTrue(r
.json())
367 self
.assertIn('zone.rpz.', content
)
368 zone
= content
['zone.rpz.']
369 for key
in ['last_update', 'records', 'serial', 'transfers_failed', 'transfers_full', 'transfers_success']:
370 self
.assertIn(key
, zone
)
372 self
.assertEqual(zone
['serial'], serial
)
373 self
.assertEqual(zone
['records'], recordsCount
)
374 self
.assertEqual(zone
['transfers_full'], fullXFRCount
)
375 self
.assertEqual(zone
['transfers_success'], totalXFRCount
)
378 rpzServer
= RPZServer(rpzServerPort
)
380 class RPZXFRRecursorTest(RPZRecursorTest
):
382 This test makes sure that we correctly update RPZ zones via AXFR then IXFR
386 _lua_config_file
= """
387 -- The first server is a bogus one, to test that we correctly fail over to the second one
388 rpzMaster({'127.0.0.1:9999', '127.0.0.1:%d'}, 'zone.rpz.', { refresh=1, includeSOA=true})
389 """ % (rpzServerPort
)
393 _wsPassword
= 'secretpassword'
394 _apiKey
= 'secretapikey'
395 _config_template
= """
396 auth-zones=example=configs/%s/example.zone
399 webserver-address=127.0.0.1
400 webserver-password=%s
403 allow-notify-from=127.0.0.0/8
404 allow-notify-for=zone.rpz
405 """ % (_confdir
, _wsPort
, _wsPassword
, _apiKey
)
409 def generateRecursorConfig(cls
, confdir
):
410 authzonepath
= os
.path
.join(confdir
, 'example.zone')
411 with
open(authzonepath
, 'w') as authzone
:
412 authzone
.write("""$ORIGIN example.
414 a 3600 IN A 192.0.2.42
415 b 3600 IN A 192.0.2.42
416 c 3600 IN A 192.0.2.42
417 d 3600 IN A 192.0.2.42
418 e 3600 IN A 192.0.2.42
419 """.format(soa
=cls
._SOA
))
420 super(RPZRecursorTest
, cls
).generateRecursorConfig(confdir
)
422 def waitUntilCorrectSerialIsLoaded(self
, serial
, timeout
=5):
425 rpzServer
.moveToSerial(serial
)
428 while attempts
< timeout
:
429 currentSerial
= rpzServer
.getCurrentSerial()
430 if currentSerial
> serial
:
431 raise AssertionError("Expected serial %d, got %d" % (serial
, currentSerial
))
432 if currentSerial
== serial
:
433 self
._xfrDone
= self
._xfrDone
+ 1
436 attempts
= attempts
+ 1
439 raise AssertionError("Waited %d seconds for the serial to be updated to %d but the serial is still %d" % (timeout
, serial
, currentSerial
))
442 # Fresh RPZ does not need a notify
443 self
.waitForTCPSocket("127.0.0.1", self
._wsPort
)
444 # first zone, only a should be blocked
445 self
.waitUntilCorrectSerialIsLoaded(1)
446 self
.checkRPZStats(1, 1, 1, self
._xfrDone
)
447 self
.checkBlocked('a.example.', soa
=True)
448 self
.checkNotBlocked('b.example.')
449 self
.checkNotBlocked('c.example.')
451 # second zone, a and b should be blocked
453 self
.waitUntilCorrectSerialIsLoaded(2)
454 self
.checkRPZStats(2, 2, 1, self
._xfrDone
)
455 self
.checkBlocked('a.example.', soa
=True)
456 self
.checkBlocked('b.example.', soa
=True)
457 self
.checkNotBlocked('c.example.')
459 # third zone, only b should be blocked
461 self
.waitUntilCorrectSerialIsLoaded(3)
462 self
.checkRPZStats(3, 1, 1, self
._xfrDone
)
463 self
.checkNotBlocked('a.example.')
464 self
.checkBlocked('b.example.', soa
=True)
465 self
.checkNotBlocked('c.example.')
467 # fourth zone, only c should be blocked
469 self
.waitUntilCorrectSerialIsLoaded(4)
470 self
.checkRPZStats(4, 1, 1, self
._xfrDone
)
471 self
.checkNotBlocked('a.example.')
472 self
.checkNotBlocked('b.example.')
473 self
.checkBlocked('c.example.', soa
=True)
475 # fifth zone, we should get a full AXFR this time, and only d should be blocked
477 self
.waitUntilCorrectSerialIsLoaded(5)
478 self
.checkRPZStats(5, 3, 2, self
._xfrDone
)
479 self
.checkNotBlocked('a.example.')
480 self
.checkNotBlocked('b.example.')
481 self
.checkNotBlocked('c.example.')
482 self
.checkBlocked('d.example.', soa
=True)
484 # sixth zone, only e should be blocked, f is a local data record
486 self
.waitUntilCorrectSerialIsLoaded(6)
487 self
.checkRPZStats(6, 2, 2, self
._xfrDone
)
488 self
.checkNotBlocked('a.example.')
489 self
.checkNotBlocked('b.example.')
490 self
.checkNotBlocked('c.example.')
491 self
.checkNotBlocked('d.example.')
492 self
.checkCustom('e.example.', 'A', dns
.rrset
.from_text('e.example.', 0, dns
.rdataclass
.IN
, 'A', '192.0.2.1', '192.0.2.2'), soa
=True)
493 self
.checkCustom('e.example.', 'MX', dns
.rrset
.from_text('e.example.', 0, dns
.rdataclass
.IN
, 'MX', '10 mx.example.'))
494 self
.checkNoData('e.example.', 'AAAA', soa
=True)
495 self
.checkCustom('f.example.', 'A', dns
.rrset
.from_text('f.example.', 0, dns
.rdataclass
.IN
, 'CNAME', 'e.example.'), soa
=True)
497 # seventh zone, e should only have one A
499 self
.waitUntilCorrectSerialIsLoaded(7)
500 self
.checkRPZStats(7, 4, 2, self
._xfrDone
)
501 self
.checkNotBlocked('a.example.')
502 self
.checkNotBlocked('b.example.')
503 self
.checkNotBlocked('c.example.')
504 self
.checkNotBlocked('d.example.')
505 self
.checkCustom('e.example.', 'A', dns
.rrset
.from_text('e.example.', 0, dns
.rdataclass
.IN
, 'A', '192.0.2.2'), soa
=True)
506 self
.checkCustom('e.example.', 'MX', dns
.rrset
.from_text('e.example.', 0, dns
.rdataclass
.IN
, 'MX', '10 mx.example.'), soa
=True)
507 self
.checkNoData('e.example.', 'AAAA', soa
=True)
508 self
.checkCustom('f.example.', 'A', dns
.rrset
.from_text('f.example.', 0, dns
.rdataclass
.IN
, 'CNAME', 'e.example.'), soa
=True)
509 # check that the policy is disabled for AD=1 queries
510 self
.checkNotBlocked('e.example.', True)
511 # check non-custom policies
512 self
.checkTruncated('tc.example.', soa
=True)
513 self
.checkDropped('drop.example.')
515 # eighth zone, all entries should be gone
517 self
.waitUntilCorrectSerialIsLoaded(8)
518 self
.checkRPZStats(8, 0, 3, self
._xfrDone
)
519 self
.checkNotBlocked('a.example.')
520 self
.checkNotBlocked('b.example.')
521 self
.checkNotBlocked('c.example.')
522 self
.checkNotBlocked('d.example.')
523 self
.checkNotBlocked('e.example.')
524 self
.checkNXD('f.example.')
525 self
.checkNXD('tc.example.')
526 self
.checkNXD('drop.example.')
528 # 9th zone is a duplicate, it might get skipped
530 rpzServer
.moveToSerial(9)
534 self
.waitUntilCorrectSerialIsLoaded(10)
535 self
.checkRPZStats(10, 1, 4, self
._xfrDone
)
536 self
.checkNotBlocked('a.example.')
537 self
.checkNotBlocked('b.example.')
538 self
.checkNotBlocked('c.example.')
539 self
.checkNotBlocked('d.example.')
540 self
.checkNotBlocked('e.example.')
541 self
.checkBlocked('f.example.', soa
=True)
542 self
.checkNXD('tc.example.')
543 self
.checkNXD('drop.example.')
545 # the next update will update the zone twice
546 rpzServer
.moveToSerial(11)
550 self
.waitUntilCorrectSerialIsLoaded(12)
551 self
.checkRPZStats(12, 1, 4, self
._xfrDone
)
552 self
.checkNotBlocked('a.example.')
553 self
.checkNotBlocked('b.example.')
554 self
.checkNotBlocked('c.example.')
555 self
.checkNotBlocked('d.example.')
556 self
.checkNotBlocked('e.example.')
557 self
.checkNXD('f.example.')
558 self
.checkBlocked('g.example.', soa
=True)
559 self
.checkNXD('tc.example.')
560 self
.checkNXD('drop.example.')
562 class RPZFileRecursorTest(RPZRecursorTest
):
564 This test makes sure that we correctly load RPZ zones from a file
568 _lua_config_file
= """
569 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz.", includeSOA=true })
571 _config_template
= """
572 auth-zones=example=configs/%s/example.zone
576 def generateRecursorConfig(cls
, confdir
):
577 authzonepath
= os
.path
.join(confdir
, 'example.zone')
578 with
open(authzonepath
, 'w') as authzone
:
579 authzone
.write("""$ORIGIN example.
581 a 3600 IN A 192.0.2.42
582 b 3600 IN A 192.0.2.42
583 c 3600 IN A 192.0.2.42
584 d 3600 IN A 192.0.2.42
585 e 3600 IN A 192.0.2.42
586 z 3600 IN A 192.0.2.42
587 """.format(soa
=cls
._SOA
))
589 rpzFilePath
= os
.path
.join(confdir
, 'zone.rpz')
590 with
open(rpzFilePath
, 'w') as rpzZone
:
591 rpzZone
.write("""$ORIGIN zone.rpz.
593 a.example.zone.rpz. 60 IN A 192.0.2.42
594 a.example.zone.rpz. 60 IN A 192.0.2.43
595 a.example.zone.rpz. 60 IN TXT "some text"
596 drop.example.zone.rpz. 60 IN CNAME rpz-drop.
597 z.example.zone.rpz. 60 IN A 192.0.2.1
598 tc.example.zone.rpz. 60 IN CNAME rpz-tcp-only.
599 """.format(soa
=cls
._SOA
))
600 super(RPZFileRecursorTest
, cls
).generateRecursorConfig(confdir
)
603 self
.checkCustom('a.example.', 'A', dns
.rrset
.from_text('a.example.', 0, dns
.rdataclass
.IN
, 'A', '192.0.2.42', '192.0.2.43'))
604 self
.checkCustom('a.example.', 'TXT', dns
.rrset
.from_text('a.example.', 0, dns
.rdataclass
.IN
, 'TXT', '"some text"'))
605 self
.checkBlocked('z.example.', soa
=True)
606 self
.checkNotBlocked('b.example.')
607 self
.checkNotBlocked('c.example.')
608 self
.checkNotBlocked('d.example.')
609 self
.checkNotBlocked('e.example.')
610 # check that the policy is disabled for AD=1 queries
611 self
.checkNotBlocked('z.example.', True)
612 # check non-custom policies
613 self
.checkTruncated('tc.example.', soa
=True)
614 self
.checkDropped('drop.example.')
616 class RPZFileDefaultPolRecursorTest(RPZRecursorTest
):
618 This test makes sure that we correctly load RPZ zones from a file with a default policy
621 _confdir
= 'RPZFileDefaultPolicy'
622 _lua_config_file
= """
623 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz.", defpol=Policy.NoAction })
625 _config_template
= """
626 auth-zones=example=configs/%s/example.zone
630 def generateRecursorConfig(cls
, confdir
):
631 authzonepath
= os
.path
.join(confdir
, 'example.zone')
632 with
open(authzonepath
, 'w') as authzone
:
633 authzone
.write("""$ORIGIN example.
635 a 3600 IN A 192.0.2.42
636 b 3600 IN A 192.0.2.42
637 c 3600 IN A 192.0.2.42
638 d 3600 IN A 192.0.2.42
639 drop 3600 IN A 192.0.2.42
640 e 3600 IN A 192.0.2.42
641 z 3600 IN A 192.0.2.42
642 """.format(soa
=cls
._SOA
))
644 rpzFilePath
= os
.path
.join(confdir
, 'zone.rpz')
645 with
open(rpzFilePath
, 'w') as rpzZone
:
646 rpzZone
.write("""$ORIGIN zone.rpz.
648 a.example.zone.rpz. 60 IN A 192.0.2.42
649 drop.example.zone.rpz. 60 IN CNAME rpz-drop.
650 z.example.zone.rpz. 60 IN A 192.0.2.1
651 tc.example.zone.rpz. 60 IN CNAME rpz-tcp-only.
652 """.format(soa
=cls
._SOA
))
653 super(RPZFileDefaultPolRecursorTest
, cls
).generateRecursorConfig(confdir
)
656 # local data entries are overridden by default
657 self
.checkCustom('a.example.', 'A', dns
.rrset
.from_text('a.example.', 0, dns
.rdataclass
.IN
, 'A', '192.0.2.42'))
658 self
.checkNoData('a.example.', 'TXT')
659 # will not be blocked because the default policy overrides local data entries by default
660 self
.checkNotBlocked('z.example.')
661 self
.checkNotBlocked('b.example.')
662 self
.checkNotBlocked('c.example.')
663 self
.checkNotBlocked('d.example.')
664 self
.checkNotBlocked('e.example.')
665 # check non-local policies, they should be overridden by the default policy
666 self
.checkNXD('tc.example.', 'A')
667 self
.checkNotBlocked('drop.example.')
669 class RPZFileDefaultPolNotOverrideLocalRecursorTest(RPZRecursorTest
):
671 This test makes sure that we correctly load RPZ zones from a file with a default policy, not overriding local data entries
674 _confdir
= 'RPZFileDefaultPolicyNotOverrideLocal'
675 _lua_config_file
= """
676 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz.", defpol=Policy.NoAction, defpolOverrideLocalData=false })
678 _config_template
= """
679 auth-zones=example=configs/%s/example.zone
683 def generateRecursorConfig(cls
, confdir
):
684 authzonepath
= os
.path
.join(confdir
, 'example.zone')
685 with
open(authzonepath
, 'w') as authzone
:
686 authzone
.write("""$ORIGIN example.
688 a 3600 IN A 192.0.2.42
689 b 3600 IN A 192.0.2.42
690 c 3600 IN A 192.0.2.42
691 d 3600 IN A 192.0.2.42
692 drop 3600 IN A 192.0.2.42
693 e 3600 IN A 192.0.2.42
694 z 3600 IN A 192.0.2.42
695 """.format(soa
=cls
._SOA
))
697 rpzFilePath
= os
.path
.join(confdir
, 'zone.rpz')
698 with
open(rpzFilePath
, 'w') as rpzZone
:
699 rpzZone
.write("""$ORIGIN zone.rpz.
701 a.example.zone.rpz. 60 IN A 192.0.2.42
702 a.example.zone.rpz. 60 IN A 192.0.2.43
703 a.example.zone.rpz. 60 IN TXT "some text"
704 drop.example.zone.rpz. 60 IN CNAME rpz-drop.
705 z.example.zone.rpz. 60 IN A 192.0.2.1
706 tc.example.zone.rpz. 60 IN CNAME rpz-tcp-only.
707 """.format(soa
=cls
._SOA
))
708 super(RPZFileDefaultPolNotOverrideLocalRecursorTest
, cls
).generateRecursorConfig(confdir
)
711 # local data entries will not be overridden by the default policy
712 self
.checkCustom('a.example.', 'A', dns
.rrset
.from_text('a.example.', 0, dns
.rdataclass
.IN
, 'A', '192.0.2.42', '192.0.2.43'))
713 self
.checkCustom('a.example.', 'TXT', dns
.rrset
.from_text('a.example.', 0, dns
.rdataclass
.IN
, 'TXT', '"some text"'))
714 # will be blocked because the default policy does not override local data entries
715 self
.checkBlocked('z.example.')
716 self
.checkNotBlocked('b.example.')
717 self
.checkNotBlocked('c.example.')
718 self
.checkNotBlocked('d.example.')
719 self
.checkNotBlocked('e.example.')
720 # check non-local policies, they should be overridden by the default policy
721 self
.checkNXD('tc.example.', 'A')
722 self
.checkNotBlocked('drop.example.')
724 class RPZSimpleAuthServer(object):
726 def __init__(self
, port
):
727 self
._serverPort
= port
728 listener
= threading
.Thread(name
='RPZ Simple Auth Listener', target
=self
._listener
, args
=[])
729 listener
.daemon
= True
732 def _getAnswer(self
, message
):
734 response
= dns
.message
.make_response(message
)
735 response
.flags |
= dns
.flags
.AA
737 dns
.rrset
.from_text('nsip.delegated.example.', 60, dns
.rdataclass
.IN
, dns
.rdatatype
.A
, '192.0.2.42')
740 response
.answer
= records
744 sock
= socket
.socket(socket
.AF_INET
, socket
.SOCK_DGRAM
)
746 sock
.bind(("127.0.0.1", self
._serverPort
))
747 except socket
.error
as e
:
748 print("Error binding in the RPZ simple auth listener: %s" % str(e
))
753 data
, addr
= sock
.recvfrom(4096)
754 message
= dns
.message
.from_wire(data
)
755 if len(message
.question
) != 1:
756 print('Invalid query, qdcount is %d' % (len(message
.question
)))
759 answer
= self
._getAnswer
(message
)
761 print('Unable to get a response for %s %d' % (message
.question
[0].name
, message
.question
[0].rdtype
))
764 wire
= answer
.to_wire()
765 sock
.sendto(wire
, addr
)
767 except socket
.error
as e
:
768 print('Error in RPZ simple auth socket: %s' % str(e
))
770 rpzAuthServerPort
= 4260
771 rpzAuthServer
= RPZSimpleAuthServer(rpzAuthServerPort
)
773 class RPZOrderingPrecedenceRecursorTest(RPZRecursorTest
):
775 This test makes sure that the recursor respects the RPZ ordering precedence rules
778 _confdir
= 'RPZOrderingPrecedence'
779 _lua_config_file
= """
780 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz."})
781 rpzFile('configs/%s/zone2.rpz', { policyName="zone2.rpz."})
782 """ % (_confdir
, _confdir
)
783 _config_template
= """
784 auth-zones=example=configs/%s/example.zone
785 forward-zones=delegated.example=127.0.0.1:%d
786 """ % (_confdir
, rpzAuthServerPort
)
789 def generateRecursorConfig(cls
, confdir
):
790 authzonepath
= os
.path
.join(confdir
, 'example.zone')
791 with
open(authzonepath
, 'w') as authzone
:
792 authzone
.write("""$ORIGIN example.
794 sub.test 3600 IN A 192.0.2.42
795 passthru-then-blocked-by-higher 3600 IN A 192.0.2.66
796 passthru-then-blocked-by-same 3600 IN A 192.0.2.66
797 blocked-then-passhtru-by-higher 3600 IN A 192.0.2.100
798 """.format(soa
=cls
._SOA
))
800 rpzFilePath
= os
.path
.join(confdir
, 'zone.rpz')
801 with
open(rpzFilePath
, 'w') as rpzZone
:
802 rpzZone
.write("""$ORIGIN zone.rpz.
804 *.test.example.zone.rpz. 60 IN CNAME rpz-passthru.
805 32.66.2.0.192.rpz-ip.zone.rpz. 60 IN A 192.0.2.1
806 32.100.2.0.192.rpz-ip.zone.rpz. 60 IN CNAME rpz-passthru.
807 passthru-then-blocked-by-same.example.zone.rpz. 60 IN CNAME rpz-passthru.
808 32.1.0.0.127.rpz-nsip.zone.rpz. 60 IN CNAME rpz-passthru.
809 """.format(soa
=cls
._SOA
))
811 rpzFilePath
= os
.path
.join(confdir
, 'zone2.rpz')
812 with
open(rpzFilePath
, 'w') as rpzZone
:
813 rpzZone
.write("""$ORIGIN zone2.rpz.
815 sub.test.example.com.zone2.rpz. 60 IN CNAME .
816 passthru-then-blocked-by-higher.example.zone2.rpz. 60 IN CNAME rpz-passthru.
817 blocked-then-passhtru-by-higher.example.zone2.rpz. 60 IN A 192.0.2.1
818 32.42.2.0.192.rpz-ip 60 IN CNAME .
819 """.format(soa
=cls
._SOA
))
821 super(RPZOrderingPrecedenceRecursorTest
, cls
).generateRecursorConfig(confdir
)
823 def testRPZOrderingForQNameAndWhitelisting(self
):
824 # we should first match on the qname (the wildcard, not on the exact name since
825 # we respect the order of the RPZ zones), see the pass-thru rule
826 # and only process RPZ rules of higher precedence.
827 # The subsequent rule on the content of the A should therefore not trigger a NXDOMAIN.
828 self
.checkNotBlocked('sub.test.example.')
830 def testRPZOrderingWhitelistedThenBlockedByHigher(self
):
831 # we should first match on the qname from the second RPZ zone,
832 # continue the resolution process, and get blocked by the content of the A record
833 # based on the first RPZ zone, whose priority is higher than the second one.
834 self
.checkBlocked('passthru-then-blocked-by-higher.example.')
836 def testRPZOrderingWhitelistedThenBlockedBySame(self
):
837 # we should first match on the qname from the first RPZ zone,
838 # continue the resolution process, and NOT get blocked by the content of the A record
839 # based on the same RPZ zone, since it's not higher.
840 self
.checkCustom('passthru-then-blocked-by-same.example.', 'A', dns
.rrset
.from_text('passthru-then-blocked-by-same.example.', 0, dns
.rdataclass
.IN
, 'A', '192.0.2.66'))
842 def testRPZOrderBlockedThenWhitelisted(self
):
843 # The qname is first blocked by the second RPZ zone
844 # Then, should the resolution process go on, the A record would be whitelisted
846 # This is what the RPZ specification requires, but we currently decided that we
847 # don't want to leak queries to malicious DNS servers and waste time if the qname is blacklisted.
848 # We might change our opinion at some point, though.
849 self
.checkBlocked('blocked-then-passhtru-by-higher.example.')
851 def testRPZOrderDelegate(self
):
852 # The IP of the NS we are going to contact is whitelisted (passthru) in zone 1,
853 # so even though the record (192.0.2.42) returned by the server is blacklisted
854 # by zone 2, it should not be blocked.
855 # We only test once because after that the answer is cached, so the NS is not contacted
856 # and the whitelist is not applied (yes, NSIP and NSDNAME are brittle).
857 self
.checkNotBlocked('nsip.delegated.example.', singleCheck
=True)
859 class RPZNSIPCustomTest(RPZRecursorTest
):
861 This test makes sure that the recursor handles custom RPZ rules in a NSIP
864 _confdir
= 'RPZNSIPCustom'
865 _lua_config_file
= """
866 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz."})
867 rpzFile('configs/%s/zone2.rpz', { policyName="zone2.rpz."})
868 """ % (_confdir
, _confdir
)
869 _config_template
= """
870 auth-zones=example=configs/%s/example.zone
871 forward-zones=delegated.example=127.0.0.1:%d
872 """ % (_confdir
, rpzAuthServerPort
)
875 def generateRecursorConfig(cls
, confdir
):
876 authzonepath
= os
.path
.join(confdir
, 'example.zone')
877 with
open(authzonepath
, 'w') as authzone
:
878 authzone
.write("""$ORIGIN example.
880 """.format(soa
=cls
._SOA
))
882 rpzFilePath
= os
.path
.join(confdir
, 'zone.rpz')
883 with
open(rpzFilePath
, 'w') as rpzZone
:
884 rpzZone
.write("""$ORIGIN zone.rpz.
886 32.1.0.0.127.rpz-nsip.zone.rpz. 60 IN A 192.0.2.1
887 """.format(soa
=cls
._SOA
))
889 rpzFilePath
= os
.path
.join(confdir
, 'zone2.rpz')
890 with
open(rpzFilePath
, 'w') as rpzZone
:
891 rpzZone
.write("""$ORIGIN zone2.rpz.
893 32.1.2.0.192.rpz-ip 60 IN CNAME .
894 """.format(soa
=cls
._SOA
))
896 super(RPZNSIPCustomTest
, cls
).generateRecursorConfig(confdir
)
898 def testRPZDelegate(self
):
899 # The IP of the NS we are going to contact should result in a custom record (192.0.2.1) from zone 1,
900 # so even though the record (192.0.2.1) returned by the server is blacklisted
901 # by zone 2, it should not be blocked.
902 # We only test once because after that the answer is cached, so the NS is not contacted
903 # and the whitelist is not applied (yes, NSIP and NSDNAME are brittle).
904 self
.checkCustom('nsip.delegated.example.', 'A', dns
.rrset
.from_text('nsip.delegated.example.', 0, dns
.rdataclass
.IN
, 'A', '192.0.2.1'))
907 class RPZResponseIPCNameChainCustomTest(RPZRecursorTest
):
909 This test makes sure that the recursor applies response IP rules to records in a CNAME chain,
910 and resolves the target of a custom CNAME.
913 _confdir
= 'RPZResponseIPCNameChainCustom'
914 _lua_config_file
= """
915 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz."})
917 _config_template
= """
918 auth-zones=example=configs/%s/example.zone
919 forward-zones=delegated.example=127.0.0.1:%d
920 """ % (_confdir
, rpzAuthServerPort
)
923 def generateRecursorConfig(cls
, confdir
):
924 authzonepath
= os
.path
.join(confdir
, 'example.zone')
925 with
open(authzonepath
, 'w') as authzone
:
926 authzone
.write("""$ORIGIN example.
929 cname IN A 192.0.2.255
930 custom-target IN A 192.0.2.254
931 """.format(soa
=cls
._SOA
))
933 rpzFilePath
= os
.path
.join(confdir
, 'zone.rpz')
934 with
open(rpzFilePath
, 'w') as rpzZone
:
935 rpzZone
.write("""$ORIGIN zone.rpz.
937 cname.example IN CNAME custom-target.example.
938 custom-target.example IN A 192.0.2.253
939 """.format(soa
=cls
._SOA
))
941 super(RPZResponseIPCNameChainCustomTest
, cls
).generateRecursorConfig(confdir
)
943 def testRPZChain(self
):
944 # we request the A record for 'name.example.', which is a CNAME to 'cname.example'
945 # this one does exist but we have a RPZ rule that should be triggered,
946 # replacing the 'real' CNAME by a CNAME to 'custom-target.example.'
947 # There is a RPZ rule for that name but it should not be triggered, since
948 # the RPZ specs state "Recall that only one policy rule, from among all those matched at all
949 # stages of resolving a CNAME or DNAME chain, can affect the final
950 # response; this is true even if the selected rule has a PASSTHRU
951 # action" in 5.1 "CNAME or DNAME Chain Position" Precedence Rule
953 # two times to check the cache
955 query
= dns
.message
.make_query('name.example.', 'A', want_dnssec
=True)
956 query
.flags |
= dns
.flags
.CD
957 for method
in ("sendUDPQuery", "sendTCPQuery"):
958 sender
= getattr(self
, method
)
960 self
.assertRcodeEqual(res
, dns
.rcode
.NOERROR
)
961 self
.assertRRsetInAnswer(res
, dns
.rrset
.from_text('name.example.', 0, dns
.rdataclass
.IN
, 'CNAME', 'cname.example.'))
962 self
.assertRRsetInAnswer(res
, dns
.rrset
.from_text('cname.example.', 0, dns
.rdataclass
.IN
, 'CNAME', 'custom-target.example.'))
963 self
.assertRRsetInAnswer(res
, dns
.rrset
.from_text('custom-target.example.', 0, dns
.rdataclass
.IN
, 'A', '192.0.2.254'))
966 class RPZCNameChainCustomTest(RPZRecursorTest
):
968 This test makes sure that the recursor applies QName rules to names in a CNAME chain.
969 No forward or internal auth zones here, as we want to test the real resolution
970 (with QName Minimization).
973 _PREFIX
= os
.environ
['PREFIX']
974 _confdir
= 'RPZCNameChainCustom'
975 _lua_config_file
= """
976 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz."})
978 _config_template
= ""
981 def generateRecursorConfig(cls
, confdir
):
982 rpzFilePath
= os
.path
.join(confdir
, 'zone.rpz')
983 with
open(rpzFilePath
, 'w') as rpzZone
:
984 rpzZone
.write("""$ORIGIN zone.rpz.
986 32.100.2.0.192.rpz-ip IN CNAME .
987 32.101.2.0.192.rpz-ip IN CNAME *.
988 32.102.2.0.192.rpz-ip IN A 192.0.2.103
989 """.format(soa
=cls
._SOA
))
991 super(RPZCNameChainCustomTest
, cls
).generateRecursorConfig(confdir
)
993 def testRPZChainNXD(self
):
994 # we should match the A at the end of the CNAME chain and
997 # two times to check the cache
999 query
= dns
.message
.make_query('cname-nxd.example.', 'A', want_dnssec
=True)
1000 query
.flags |
= dns
.flags
.CD
1001 for method
in ("sendUDPQuery", "sendTCPQuery"):
1002 sender
= getattr(self
, method
)
1004 self
.assertRcodeEqual(res
, dns
.rcode
.NXDOMAIN
)
1005 self
.assertEqual(len(res
.answer
), 0)
1007 def testRPZChainNODATA(self
):
1008 # we should match the A at the end of the CNAME chain and
1011 # two times to check the cache
1013 query
= dns
.message
.make_query('cname-nodata.example.', 'A', want_dnssec
=True)
1014 query
.flags |
= dns
.flags
.CD
1015 for method
in ("sendUDPQuery", "sendTCPQuery"):
1016 sender
= getattr(self
, method
)
1018 self
.assertRcodeEqual(res
, dns
.rcode
.NOERROR
)
1019 self
.assertEqual(len(res
.answer
), 0)
1021 def testRPZChainCustom(self
):
1022 # we should match the A at the end of the CNAME chain and
1023 # get a custom A, replacing the existing one
1025 # two times to check the cache
1027 query
= dns
.message
.make_query('cname-custom-a.example.', 'A', want_dnssec
=True)
1028 query
.flags |
= dns
.flags
.CD
1029 for method
in ("sendUDPQuery", "sendTCPQuery"):
1030 sender
= getattr(self
, method
)
1032 self
.assertRcodeEqual(res
, dns
.rcode
.NOERROR
)
1033 # the original CNAME record is signed
1034 self
.assertEqual(len(res
.answer
), 3)
1035 self
.assertRRsetInAnswer(res
, dns
.rrset
.from_text('cname-custom-a.example.', 0, dns
.rdataclass
.IN
, 'CNAME', 'cname-custom-a-target.example.'))
1036 self
.assertRRsetInAnswer(res
, dns
.rrset
.from_text('cname-custom-a-target.example.', 0, dns
.rdataclass
.IN
, 'A', '192.0.2.103'))
1038 class RPZFileModByLuaRecursorTest(RPZRecursorTest
):
1040 This test makes sure that we correctly load RPZ zones from a file while being modified by Lua callbacks
1043 _confdir
= 'RPZFileModByLua'
1044 _lua_dns_script_file
= """
1045 function preresolve(dq)
1046 if dq.qname:equal('zmod.example.') then
1047 dq.appliedPolicy.policyKind = pdns.policykinds.Drop
1052 function nxdomain(dq)
1053 if dq.qname:equal('nxmod.example.') then
1054 dq.appliedPolicy.policyKind = pdns.policykinds.Drop
1061 if dq.qname:equal('nodatamod.example.') then
1062 dq.appliedPolicy.policyKind = pdns.policykinds.Drop
1068 _lua_config_file
= """
1069 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz." })
1071 _config_template
= """
1072 auth-zones=example=configs/%s/example.zone
1076 def generateRecursorConfig(cls
, confdir
):
1077 authzonepath
= os
.path
.join(confdir
, 'example.zone')
1078 with
open(authzonepath
, 'w') as authzone
:
1079 authzone
.write("""$ORIGIN example.
1081 a 3600 IN A 192.0.2.42
1082 b 3600 IN A 192.0.2.42
1083 c 3600 IN A 192.0.2.42
1084 d 3600 IN A 192.0.2.42
1085 e 3600 IN A 192.0.2.42
1086 z 3600 IN A 192.0.2.42
1087 """.format(soa
=cls
._SOA
))
1089 rpzFilePath
= os
.path
.join(confdir
, 'zone.rpz')
1090 with
open(rpzFilePath
, 'w') as rpzZone
:
1091 rpzZone
.write("""$ORIGIN zone.rpz.
1093 a.example.zone.rpz. 60 IN A 192.0.2.42
1094 a.example.zone.rpz. 60 IN A 192.0.2.43
1095 a.example.zone.rpz. 60 IN TXT "some text"
1096 drop.example.zone.rpz. 60 IN CNAME rpz-drop.
1097 zmod.example.zone.rpz. 60 IN A 192.0.2.1
1098 tc.example.zone.rpz. 60 IN CNAME rpz-tcp-only.
1099 nxmod.exmaple.zone.rpz. 60 in CNAME .
1100 nodatamod.example.zone.rpz. 60 in CNAME *.
1101 """.format(soa
=cls
._SOA
))
1102 super(RPZFileModByLuaRecursorTest
, cls
).generateRecursorConfig(confdir
)
1105 self
.checkCustom('a.example.', 'A', dns
.rrset
.from_text('a.example.', 0, dns
.rdataclass
.IN
, 'A', '192.0.2.42', '192.0.2.43'))
1106 self
.checkCustom('a.example.', 'TXT', dns
.rrset
.from_text('a.example.', 0, dns
.rdataclass
.IN
, 'TXT', '"some text"'))
1107 self
.checkDropped('zmod.example.')
1108 self
.checkDropped('nxmod.example.')
1109 self
.checkDropped('nodatamod.example.')
1110 self
.checkNotBlocked('b.example.')
1111 self
.checkNotBlocked('c.example.')
1112 self
.checkNotBlocked('d.example.')
1113 self
.checkNotBlocked('e.example.')
1114 # check non-custom policies
1115 self
.checkTruncated('tc.example.')
1116 self
.checkDropped('drop.example.')