]> git.ipfire.org Git - thirdparty/pdns.git/blob - regression-tests.recursor-dnssec/test_Lua.py
Merge pull request #13904 from jsoref/issue-menu
[thirdparty/pdns.git] / regression-tests.recursor-dnssec / test_Lua.py
1 import clientsubnetoption
2 import cookiesoption
3 import dns
4 import os
5 import threading
6 import time
7
8 from twisted.internet.protocol import DatagramProtocol
9 from twisted.internet import reactor
10
11 from recursortests import RecursorTest
12
13 class GettagRecursorTest(RecursorTest):
14 _confdir = 'LuaGettag'
15 _config_template = """
16 log-common-errors=yes
17 gettag-needs-edns-options=yes
18 """
19 _lua_dns_script_file = """
20 function gettag(remote, ednssubnet, localip, qname, qtype, ednsoptions, tcp)
21
22 local tags = {}
23 local data = {}
24
25 -- make sure we can pass data around to the other hooks
26 data['canary'] = 'from-gettag'
27
28 -- test that the remote addr is valid
29 if remote:toString() ~= '127.0.0.1' then
30 pdnslog("invalid remote")
31 table.insert(tags, 'invalid remote '..remote:toString())
32 return 1, tags, data
33 end
34
35 -- test that the local addr is valid
36 if localip:toString() ~= '127.0.0.1' then
37 pdnslog("invalid local")
38 table.insert(tags, 'invalid local '..localip:toString())
39 return 1, tags, data
40 end
41
42 if not ednssubnet:empty() then
43 table.insert(tags, 'edns-subnet-'..ednssubnet:toString())
44 end
45
46 for k,v in pairs(ednsoptions) do
47 table.insert(tags, 'ednsoption-'..k..'-count-'..v:count())
48 local len = 0
49 local values = v:getValues()
50 for j,l in pairs(values) do
51 len = len + l:len()
52
53 -- check that the old interface (before 4.2.0) still works
54 if j == 0 then
55 if l:len() ~= v.size then
56 table.insert(tags, 'size obtained via the old edns option interface does not match')
57 end
58 value = v:getContent()
59 if value ~= l then
60 table.insert(tags, 'content obtained via the old edns option interface does not match')
61 end
62 end
63 end
64 table.insert(tags, 'ednsoption-'..k..'-total-len-'..len)
65 end
66
67 if tcp then
68 table.insert(tags, 'gettag-tcp')
69 end
70
71 -- test that tags are passed to other hooks
72 table.insert(tags, qname:toString())
73 table.insert(tags, 'gettag-qtype-'..qtype)
74
75 return 0, tags, data
76 end
77
78 function preresolve(dq)
79
80 -- test that we are getting the tags set by gettag()
81 -- and also getting the correct qname
82 local found = false
83 for _, tag in pairs(dq:getPolicyTags()) do
84 if dq.qname:equal(tag) then
85 found = true
86 end
87 dq:addAnswer(pdns.TXT, '"'..tag..'"')
88 end
89
90 if not found then
91 pdnslog("not valid tag found")
92 dq.rcode = pdns.REFUSED
93 return true
94 end
95
96 if dq.data['canary'] ~= 'from-gettag' then
97 pdnslog("did not get any data from gettag")
98 dq.rcode = pdns.REFUSED
99 return true
100 end
101
102 local tm = os.time()
103 if dq.queryTime.tv_sec < tm - 1 or dq.queryTime.tv_sec > tm + 1 then
104 pdnslog("queryTime is wrong")
105 dq.rcode = pdns.REFUSED
106 return true
107 end
108
109 if dq.qtype == pdns.A then
110 dq:addAnswer(pdns.A, '192.0.2.1')
111 elseif dq.qtype == pdns.AAAA then
112 dq:addAnswer(pdns.AAAA, '2001:db8::1')
113 end
114
115 return true
116 end
117 """
118
119 @classmethod
120 def setUpClass(cls):
121
122 cls.setUpSockets()
123 confdir = os.path.join('configs', cls._confdir)
124 cls.createConfigDir(confdir)
125 cls.generateRecursorConfig(confdir)
126 cls.startRecursor(confdir, cls._recursorPort)
127
128 @classmethod
129 def tearDownClass(cls):
130 cls.tearDownRecursor()
131
132 def testA(self):
133 name = 'gettag.lua.'
134 expected = [
135 dns.rrset.from_text(name, 0, dns.rdataclass.IN, 'A', '192.0.2.1'),
136 dns.rrset.from_text_list(name, 0, dns.rdataclass.IN, 'TXT', [ name, 'gettag-qtype-1'])
137 ]
138 query = dns.message.make_query(name, 'A', want_dnssec=True)
139 query.flags |= dns.flags.CD
140 res = self.sendUDPQuery(query)
141 self.assertResponseMatches(query, expected, res)
142
143 def testTCPA(self):
144 name = 'gettag-tcpa.lua.'
145 expected = [
146 dns.rrset.from_text(name, 0, dns.rdataclass.IN, 'A', '192.0.2.1'),
147 dns.rrset.from_text_list(name, 0, dns.rdataclass.IN, 'TXT', [ name, 'gettag-qtype-1', 'gettag-tcp'])
148 ]
149 query = dns.message.make_query(name, 'A', want_dnssec=True)
150 query.flags |= dns.flags.CD
151 res = self.sendTCPQuery(query)
152 self.assertResponseMatches(query, expected, res)
153
154 def testAAAA(self):
155 name = 'gettag-aaaa.lua.'
156 expected = [
157 dns.rrset.from_text(name, 0, dns.rdataclass.IN, 'AAAA', '2001:db8::1'),
158 dns.rrset.from_text_list(name, 0, dns.rdataclass.IN, 'TXT', [ name, 'gettag-qtype-28'])
159 ]
160 query = dns.message.make_query(name, 'AAAA', want_dnssec=True)
161 query.flags |= dns.flags.CD
162 res = self.sendUDPQuery(query)
163 self.assertResponseMatches(query, expected, res)
164
165 def testAAAA(self):
166 name = 'gettag-tcpaaaa.lua.'
167 expected = [
168 dns.rrset.from_text(name, 0, dns.rdataclass.IN, 'AAAA', '2001:db8::1'),
169 dns.rrset.from_text_list(name, 0, dns.rdataclass.IN, 'TXT', [ name, 'gettag-qtype-28', 'gettag-tcp'])
170 ]
171 query = dns.message.make_query(name, 'AAAA', want_dnssec=True)
172 query.flags |= dns.flags.CD
173 res = self.sendTCPQuery(query)
174 self.assertResponseMatches(query, expected, res)
175
176 def testSubnet(self):
177 name = 'gettag-subnet.lua.'
178 subnet = '192.0.2.255'
179 subnetMask = 32
180 ecso = clientsubnetoption.ClientSubnetOption(subnet, subnetMask)
181 expected = [
182 dns.rrset.from_text(name, 0, dns.rdataclass.IN, 'A', '192.0.2.1'),
183 dns.rrset.from_text_list(name, 0, dns.rdataclass.IN, 'TXT', [name, 'gettag-qtype-1', 'edns-subnet-' + subnet + '/' + str(subnetMask),
184 'ednsoption-8-count-1', 'ednsoption-8-total-len-8']),
185 ]
186 query = dns.message.make_query(name, 'A', want_dnssec=True, options=[ecso])
187 query.flags |= dns.flags.CD
188 res = self.sendUDPQuery(query)
189 self.assertResponseMatches(query, expected, res)
190
191 def testEDNSOptions(self):
192 name = 'gettag-ednsoptions.lua.'
193 subnet = '192.0.2.255'
194 subnetMask = 32
195 ecso = clientsubnetoption.ClientSubnetOption(subnet, subnetMask)
196 eco1 = cookiesoption.CookiesOption(b'deadbeef', b'deadbeef')
197 eco2 = cookiesoption.CookiesOption(b'deadc0de', b'deadc0de')
198
199 expected = [
200 dns.rrset.from_text(name, 0, dns.rdataclass.IN, 'A', '192.0.2.1'),
201 dns.rrset.from_text_list(name, 0, dns.rdataclass.IN, 'TXT', [name, 'gettag-qtype-1', 'edns-subnet-' + subnet + '/' + str(subnetMask),
202 'ednsoption-10-count-2', 'ednsoption-10-total-len-32',
203 'ednsoption-8-count-1', 'ednsoption-8-total-len-8'
204 ]),
205 ]
206 query = dns.message.make_query(name, 'A', want_dnssec=True, options=[eco1,ecso,eco2])
207 query.flags |= dns.flags.CD
208 res = self.sendUDPQuery(query)
209 self.assertResponseMatches(query, expected, res)
210
211 class GettagRecursorDistributesQueriesTest(GettagRecursorTest):
212 _confdir = 'LuaGettagDistributes'
213 _config_template = """
214 log-common-errors=yes
215 gettag-needs-edns-options=yes
216 pdns-distributes-queries=yes
217 threads=2
218 """
219
220 hooksReactorRunning = False
221
222 class UDPHooksResponder(DatagramProtocol):
223
224 def datagramReceived(self, datagram, address):
225 request = dns.message.from_wire(datagram)
226
227 response = dns.message.make_response(request)
228 response.flags |= dns.flags.AA
229
230 if request.question[0].name == dns.name.from_text('nxdomain.luahooks.example.'):
231 soa = dns.rrset.from_text('luahooks.example.', 86400, dns.rdataclass.IN, 'SOA', 'ns.luahooks.example. hostmaster.luahooks.example. 1 3600 3600 3600 1')
232 response.authority.append(soa)
233 response.set_rcode(dns.rcode.NXDOMAIN)
234
235 elif request.question[0].name == dns.name.from_text('nodata.luahooks.example.'):
236 soa = dns.rrset.from_text('luahooks.example.', 86400, dns.rdataclass.IN, 'SOA', 'ns.luahooks.example. hostmaster.luahooks.example. 1 3600 3600 3600 1')
237 response.authority.append(soa)
238
239 elif request.question[0].name == dns.name.from_text('postresolve.luahooks.example.'):
240 answer = dns.rrset.from_text('postresolve.luahooks.example.', 3600, dns.rdataclass.IN, 'A', '192.0.2.1')
241 response.answer.append(answer)
242
243 elif request.question[0].name == dns.name.from_text('preresolve.luahooks.example.'):
244 answer = dns.rrset.from_text('preresolve.luahooks.example.', 3600, dns.rdataclass.IN, 'A', '192.0.2.2')
245 response.answer.append(answer)
246
247 self.transport.write(response.to_wire(), address)
248
249 class LuaHooksRecursorTest(RecursorTest):
250 _confdir = 'LuaHooks'
251 _config_template = """
252 forward-zones=luahooks.example=%s.23
253 log-common-errors=yes
254 quiet=no
255 """ % (os.environ['PREFIX'])
256 _lua_dns_script_file = """
257
258 allowedips = newNMG()
259 allowedips:addMask("%s.0/24")
260
261 function ipfilter(remoteip, localip, dh)
262 -- allow only 127.0.0.1 and AD=0
263 if allowedips:match(remoteip) and not dh:getAD() then
264 return false
265 end
266
267 return true
268 end
269
270 function preresolve(dq)
271 -- test both preresolve hooking and udpQueryResponse
272 if dq.qtype == pdns.A and dq.qname == newDN("preresolve.luahooks.example.") then
273 dq.followupFunction = "udpQueryResponse"
274 dq.udpCallback = "gotUdpResponse"
275 dq.udpQueryDest = newCA("%s.23:53")
276 -- synthesize query, responder should answer with regular answer
277 dq.udpQuery = "\\254\\255\\001\\000\\000\\001\\000\\000\\000\\000\\000\\000\\npreresolve\\008luahooks\\007example\\000\\000\\001\\000\\001"
278 return true
279 end
280 return false
281 end
282
283 function gotUdpResponse(dq)
284 dq.followupFunction = ""
285 if string.len(dq.udpAnswer) == 61 then
286 dq:addAnswer(pdns.A, "192.0.2.2")
287 return true;
288 end
289 dq:addAnswer(pdns.A, "0.0.0.0")
290 return true
291 end
292
293 function nodata(dq)
294 if dq.qtype == pdns.AAAA and dq.qname == newDN("nodata.luahooks.example.") then
295 dq:addAnswer(pdns.AAAA, "2001:DB8::1")
296 return true
297 end
298
299 return false
300 end
301
302 function nxdomain(dq)
303 if dq.qtype == pdns.A and dq.qname == newDN("nxdomain.luahooks.example.") then
304 dq.rcode=0
305 dq:addAnswer(pdns.A, "192.0.2.1")
306 return true
307 end
308
309 return false
310 end
311
312 function postresolve(dq)
313 if dq.qtype == pdns.A and dq.qname == newDN("postresolve.luahooks.example.") then
314 local records = dq:getRecords()
315 for k,v in pairs(records) do
316 if v.type == pdns.A and v:getContent() == "192.0.2.1" then
317 v:changeContent("192.0.2.42")
318 v.ttl=1
319 end
320 end
321 dq:setRecords(records)
322 return true
323 end
324
325 return false
326 end
327
328 function preoutquery(dq)
329 if dq.remoteaddr:equal(newCA("%s.23")) and dq.qname == newDN("preout.luahooks.example.") and dq.qtype == pdns.A then
330 dq.rcode = -3 -- "kill"
331 return true
332 end
333
334 return false
335 end
336
337 """ % (os.environ['PREFIX'], os.environ['PREFIX'], os.environ['PREFIX'])
338
339 @classmethod
340 def startResponders(cls):
341 global hooksReactorRunning
342 print("Launching responders..")
343
344 address = cls._PREFIX + '.23'
345 port = 53
346
347 if not hooksReactorRunning:
348 reactor.listenUDP(port, UDPHooksResponder(), interface=address)
349 hooksReactorRunning = True
350
351 if not reactor.running:
352 cls._UDPResponder = threading.Thread(name='UDP Hooks Responder', target=reactor.run, args=(False,))
353 cls._UDPResponder.setDaemon(True)
354 cls._UDPResponder.start()
355
356 @classmethod
357 def setUpClass(cls):
358 cls.setUpSockets()
359
360 cls.startResponders()
361
362 confdir = os.path.join('configs', cls._confdir)
363 cls.createConfigDir(confdir)
364
365 cls.generateRecursorConfig(confdir)
366 cls.startRecursor(confdir, cls._recursorPort)
367
368 print("Launching tests..")
369
370 @classmethod
371 def tearDownClass(cls):
372 cls.tearDownRecursor()
373
374 def testNoData(self):
375 expected = dns.rrset.from_text('nodata.luahooks.example.', 3600, dns.rdataclass.IN, 'AAAA', '2001:DB8::1')
376 query = dns.message.make_query('nodata.luahooks.example.', 'AAAA', 'IN')
377
378 for method in ("sendUDPQuery", "sendTCPQuery"):
379 sender = getattr(self, method)
380 res = sender(query)
381 self.assertRcodeEqual(res, dns.rcode.NOERROR)
382 self.assertRRsetInAnswer(res, expected)
383
384 def testVanillaNXD(self):
385 #expected = dns.rrset.from_text('nxdomain.luahooks.example.', 3600, dns.rdataclass.IN, 'A', '192.0.2.1')
386 query = dns.message.make_query('nxdomain.luahooks.example.', 'AAAA', 'IN')
387
388 for method in ("sendUDPQuery", "sendTCPQuery"):
389 sender = getattr(self, method)
390 res = sender(query)
391 self.assertRcodeEqual(res, dns.rcode.NXDOMAIN)
392
393 def testHookedNXD(self):
394 expected = dns.rrset.from_text('nxdomain.luahooks.example.', 3600, dns.rdataclass.IN, 'A', '192.0.2.1')
395 query = dns.message.make_query('nxdomain.luahooks.example.', 'A', 'IN')
396
397 for method in ("sendUDPQuery", "sendTCPQuery"):
398 sender = getattr(self, method)
399 res = sender(query)
400 self.assertRcodeEqual(res, dns.rcode.NOERROR)
401 self.assertRRsetInAnswer(res, expected)
402
403 def testPostResolve(self):
404 expected = dns.rrset.from_text('postresolve.luahooks.example.', 1, dns.rdataclass.IN, 'A', '192.0.2.42')
405 query = dns.message.make_query('postresolve.luahooks.example.', 'A', 'IN')
406
407 for method in ("sendUDPQuery", "sendTCPQuery"):
408 sender = getattr(self, method)
409 res = sender(query)
410 self.assertRcodeEqual(res, dns.rcode.NOERROR)
411 self.assertRRsetInAnswer(res, expected)
412 self.assertEqual(res.answer[0].ttl, 1)
413
414 def testPreResolve(self):
415 expected = dns.rrset.from_text('preresolve.luahooks.example.', 1, dns.rdataclass.IN, 'A', '192.0.2.2')
416 query = dns.message.make_query('preresolve.luahooks.example.', 'A', 'IN')
417 for method in ("sendUDPQuery", "sendTCPQuery"):
418 sender = getattr(self, method)
419 res = sender(query)
420 self.assertRcodeEqual(res, dns.rcode.NOERROR)
421 self.assertRRsetInAnswer(res, expected)
422
423 def testIPFilterHeader(self):
424 query = dns.message.make_query('ipfilter.luahooks.example.', 'A', 'IN')
425 query.flags |= dns.flags.AD
426
427 for method in ("sendUDPQuery", "sendTCPQuery"):
428 sender = getattr(self, method)
429 res = sender(query)
430 self.assertEqual(res, None)
431
432 def testPreOutInterceptedQuery(self):
433 query = dns.message.make_query('preout.luahooks.example.', 'A', 'IN')
434
435 for method in ("sendUDPQuery", "sendTCPQuery"):
436 sender = getattr(self, method)
437 res = sender(query)
438 self.assertRcodeEqual(res, dns.rcode.SERVFAIL)
439
440 def testPreOutNotInterceptedQuery(self):
441 query = dns.message.make_query('preout.luahooks.example.', 'AAAA', 'IN')
442
443 for method in ("sendUDPQuery", "sendTCPQuery"):
444 sender = getattr(self, method)
445 res = sender(query)
446 self.assertRcodeEqual(res, dns.rcode.NOERROR)
447
448 class LuaHooksRecursorDistributesTest(LuaHooksRecursorTest):
449 _confdir = 'LuaHooksDistributes'
450 _config_template = """
451 forward-zones=luahooks.example=%s.23
452 log-common-errors=yes
453 pdns-distributes-queries=yes
454 threads=2
455 quiet=no
456 """ % (os.environ['PREFIX'])
457
458 class LuaDNS64Test(RecursorTest):
459 """Tests the dq.followupAction("getFakeAAAARecords")"""
460
461 _confdir = 'lua-dns64'
462 _config_template = """
463 """
464 _lua_dns_script_file = """
465 prefix = "2001:DB8:64::"
466
467 function nodata (dq)
468 if dq.qtype ~= pdns.AAAA then
469 return false
470 end -- only AAAA records
471
472 -- don't fake AAAA records if DNSSEC validation failed
473 if dq.validationState == pdns.validationstates.Bogus then
474 return false
475 end
476
477 dq.followupFunction = "getFakeAAAARecords"
478 dq.followupPrefix = prefix
479 dq.followupName = dq.qname
480 return true
481 end
482 """
483
484 def testAtoAAAA(self):
485 expected = [
486 dns.rrset.from_text('ns.secure.example.', 15, dns.rdataclass.IN, 'AAAA', '2001:db8:64::7f00:9')
487 ]
488 query = dns.message.make_query('ns.secure.example', 'AAAA')
489
490 for method in ("sendUDPQuery", "sendTCPQuery"):
491 sender = getattr(self, method)
492 res = sender(query)
493
494 self.assertRcodeEqual(res, dns.rcode.NOERROR)
495 self.assertEqual(len(res.answer), 1)
496 self.assertEqual(len(res.authority), 0)
497 self.assertResponseMatches(query, expected, res)
498
499 def testAtoCNAMEtoAAAA(self):
500 expected = [
501 dns.rrset.from_text('cname-to-insecure.secure.example.', 3600, dns.rdataclass.IN, 'CNAME', 'node1.insecure.example.'),
502 dns.rrset.from_text('node1.insecure.example.', 3600, dns.rdataclass.IN, 'AAAA', '2001:db8:64::c000:206')
503 ]
504 query = dns.message.make_query('cname-to-insecure.secure.example.', 'AAAA')
505
506 for method in ("sendUDPQuery", "sendTCPQuery"):
507 sender = getattr(self, method)
508 res = sender(query)
509
510 self.assertRcodeEqual(res, dns.rcode.NOERROR)
511 self.assertEqual(len(res.answer), 2)
512 self.assertEqual(len(res.authority), 0)
513 self.assertResponseMatches(query, expected, res)
514
515 class GettagFFIDNS64Test(RecursorTest):
516 """Tests the interaction between gettag_ffi, RPZ and DNS64:
517 - gettag_ffi will intercept the query and return a NODATA
518 - the RPZ zone will match the name, but the only type is A
519 - DNS64 should kick in, generating an AAAA
520 """
521
522 _confdir = 'gettagffi-rpz-dns64'
523 _config_template = """
524 dns64-prefix=64:ff9b::/96
525 """
526 _lua_config_file = """
527 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz." })
528 """ % (_confdir)
529
530 _lua_dns_script_file = """
531 local ffi = require("ffi")
532
533 ffi.cdef[[
534 typedef struct pdns_ffi_param pdns_ffi_param_t;
535
536 void pdns_ffi_param_set_rcode(pdns_ffi_param_t* ref, int rcode);
537 ]]
538
539 function gettag_ffi(obj)
540 -- fake a NODATA (without SOA) no matter the query
541 ffi.C.pdns_ffi_param_set_rcode(obj, 0)
542 return {}
543 end
544 """
545 _auth_zones = []
546
547 @classmethod
548 def generateRecursorConfig(cls, confdir):
549 rpzFilePath = os.path.join(confdir, 'zone.rpz')
550 with open(rpzFilePath, 'w') as rpzZone:
551 rpzZone.write("""$ORIGIN zone.rpz.
552 @ 3600 IN SOA {soa}
553 dns64.test.powerdns.com.zone.rpz. 60 IN A 192.0.2.42
554 """.format(soa=cls._SOA))
555 super(GettagFFIDNS64Test, cls).generateRecursorConfig(confdir)
556
557 def testAtoAAAA(self):
558 expected = [
559 dns.rrset.from_text('dns64.test.powerdns.com.', 15, dns.rdataclass.IN, 'AAAA', '64:ff9b::c000:22a')
560 ]
561 query = dns.message.make_query('dns64.test.powerdns.com.', 'AAAA')
562
563 for method in ("sendUDPQuery", "sendTCPQuery"):
564 sender = getattr(self, method)
565 res = sender(query)
566
567 self.assertRcodeEqual(res, dns.rcode.NOERROR)
568 self.assertEqual(len(res.answer), 1)
569 self.assertEqual(len(res.authority), 0)
570 self.assertResponseMatches(query, expected, res)
571
572 class PDNSRandomTest(RecursorTest):
573 """Tests if pdnsrandom works"""
574
575 _confdir = 'pdnsrandom'
576 _config_template = """
577 """
578 _lua_dns_script_file = """
579 function preresolve (dq)
580 dq.rcode = pdns.NOERROR
581 dq:addAnswer(pdns.TXT, pdnsrandom())
582 dq.variable = true
583 return true
584 end
585 """
586
587 def testRandom(self):
588 query = dns.message.make_query('whatever.example.', 'TXT')
589
590 ans = set()
591
592 ret = self.sendUDPQuery(query)
593 ans.add(ret.answer[0][0])
594 ret = self.sendUDPQuery(query)
595 ans.add(ret.answer[0][0])
596
597 self.assertEqual(len(ans), 2)
598
599
600 class PDNSFeaturesTest(RecursorTest):
601 """Tests if pdns_features works"""
602
603 _confdir = 'pdnsfeatures'
604 _config_template = """
605 """
606 _lua_dns_script_file = """
607 function preresolve (dq)
608 dq.rcode = pdns.NOERROR
609 -- test pdns_features
610 if pdns_features['nonexistent'] ~= nil then
611 print('PDNSFeaturesTest: case 1')
612 dq.rcode = pdns.SERVFAIL
613 end
614 if not pdns_features['PR8001_devicename'] then
615 print('PDNSFeaturesTest: case 2')
616 dq.rcode = pdns.SERVFAIL
617 end
618 return true
619 end
620 """
621
622 def testFeatures(self):
623 query = dns.message.make_query('whatever.example.', 'TXT')
624 res = self.sendUDPQuery(query)
625
626 self.assertRcodeEqual(res, dns.rcode.NOERROR)
627
628 class PDNSGeneratingAnswerFromGettagTest(RecursorTest):
629 """Tests that we can generate answers from gettag"""
630
631 _confdir = 'gettaganswers'
632 _config_template = """
633 """
634 _lua_dns_script_file = """
635 local ffi = require("ffi")
636
637 ffi.cdef[[
638 typedef struct pdns_ffi_param pdns_ffi_param_t;
639
640 typedef enum
641 {
642 answer = 1,
643 authority = 2,
644 additional = 3
645 } pdns_record_place_t;
646
647 const char* pdns_ffi_param_get_qname(pdns_ffi_param_t* ref);
648 void pdns_ffi_param_set_rcode(pdns_ffi_param_t* ref, int rcode);
649 bool pdns_ffi_param_add_record(pdns_ffi_param_t *ref, const char* name, uint16_t type, uint32_t ttl, const char* content, size_t contentSize, pdns_record_place_t place);
650 ]]
651
652 function gettag_ffi(obj)
653 local qname = ffi.string(ffi.C.pdns_ffi_param_get_qname(obj))
654 if qname == 'gettag-answers.powerdns.com' then
655 ffi.C.pdns_ffi_param_set_rcode(obj, 0)
656 ffi.C.pdns_ffi_param_add_record(obj, nil, pdns.A, 60, '192.0.2.1', 9, 1);
657 ffi.C.pdns_ffi_param_add_record(obj, "not-powerdns.com.", pdns.A, 60, '192.0.2.2', 9, 3);
658 end
659 end
660
661 function preresolve (dq)
662 -- refused everything if it comes that far
663 dq.rcode = pdns.REFUSED
664 return true
665 end
666 """
667
668 def testGettag(self):
669 expectedAnswerRecords = [
670 dns.rrset.from_text('gettag-answers.powerdns.com.', 60, dns.rdataclass.IN, 'A', '192.0.2.1'),
671 ]
672 expectedAdditionalRecords = [
673 dns.rrset.from_text('not-powerdns.com.', 60, dns.rdataclass.IN, 'A', '192.0.2.2'),
674 ]
675 query = dns.message.make_query('gettag-answers.powerdns.com.', 'A')
676 res = self.sendUDPQuery(query)
677
678 self.assertRcodeEqual(res, dns.rcode.NOERROR)
679 self.assertEqual(len(res.answer), 1)
680 self.assertEqual(len(res.authority), 0)
681 self.assertEqual(len(res.additional), 1)
682 self.assertEqual(res.answer, expectedAnswerRecords)
683 self.assertEqual(res.additional, expectedAdditionalRecords)
684
685 class PDNSValidationStatesTest(RecursorTest):
686 """Tests that we have access to the validation states from Lua"""
687
688 _confdir = 'validation-states-from-lua'
689 _config_template = """
690 dnssec=validate
691 """
692 _roothints = None
693 _lua_config_file = """
694 """
695
696 _lua_dns_script_file = """
697 function postresolve (dq)
698 if pdns.validationstates.Indeterminate == nil or
699 pdns.validationstates.BogusNoValidDNSKEY == nil or
700 pdns.validationstates.BogusInvalidDenial == nil or
701 pdns.validationstates.BogusUnableToGetDSs == nil or
702 pdns.validationstates.BogusUnableToGetDNSKEYs == nil or
703 pdns.validationstates.BogusSelfSignedDS == nil or
704 pdns.validationstates.BogusNoRRSIG == nil or
705 pdns.validationstates.BogusNoValidRRSIG == nil or
706 pdns.validationstates.BogusMissingNegativeIndication == nil or
707 pdns.validationstates.BogusSignatureNotYetValid == nil or
708 pdns.validationstates.BogusSignatureExpired == nil or
709 pdns.validationstates.BogusUnsupportedDNSKEYAlgo == nil or
710 pdns.validationstates.BogusUnsupportedDSDigestType == nil or
711 pdns.validationstates.BogusNoZoneKeyBitSet == nil or
712 pdns.validationstates.BogusRevokedDNSKEY == nil or
713 pdns.validationstates.BogusInvalidDNSKEYProtocol == nil or
714 pdns.validationstates.Insecure == nil or
715 pdns.validationstates.Secure == nil or
716 pdns.validationstates.Bogus == nil then
717 -- refused if at least one state is not available
718 pdnslog('Missing DNSSEC validation state!')
719 dq.rcode = pdns.REFUSED
720 return true
721 end
722 if dq.qname == newDN('brokendnssec.net.') then
723 if dq.validationState ~= pdns.validationstates.Bogus then
724 pdnslog('DNSSEC validation state should be Bogus!')
725 dq.rcode = pdns.REFUSED
726 return true
727 end
728 if dq.detailedValidationState ~= pdns.validationstates.BogusNoRRSIG then
729 pdnslog('DNSSEC detailed validation state is not valid, got '..dq.detailedValidationState..' and expected '..pdns.validationstates.BogusNoRRSIG)
730 dq.rcode = pdns.REFUSED
731 return true
732 end
733 if not isValidationStateBogus(dq.detailedValidationState) then
734 pdnslog('DNSSEC detailed validation state should be Bogus and is not!')
735 dq.rcode = pdns.REFUSED
736 return true
737 end
738 end
739 return false
740 end
741 """
742
743 def testValidationBogus(self):
744 query = dns.message.make_query('brokendnssec.net.', 'A')
745 res = self.sendUDPQuery(query)
746 self.assertRcodeEqual(res, dns.rcode.SERVFAIL)
747 self.assertEqual(len(res.answer), 0)
748 self.assertEqual(len(res.authority), 0)
749
750 class PolicyEventFilterOnFollowUpTest(RecursorTest):
751 """Tests the interaction between RPZ and followup queries (dns64, followCNAME)
752 """
753
754 _confdir = 'policyeventfilter-followup'
755 _config_template = """
756 """
757 _lua_config_file = """
758 rpzFile('configs/%s/zone.rpz', { policyName="zone.rpz." })
759 """ % (_confdir)
760
761 _lua_dns_script_file = """
762 function preresolve(dq)
763 dq:addAnswer(pdns.CNAME, "secure.example.")
764 dq.followupFunction="followCNAMERecords"
765 dq.rcode = pdns.NOERROR
766 return true
767 end
768
769 function policyEventFilter(event)
770 event.appliedPolicy.policyKind = pdns.policykinds.NoAction
771 return true
772 end
773 """
774
775 @classmethod
776 def generateRecursorConfig(cls, confdir):
777 rpzFilePath = os.path.join(confdir, 'zone.rpz')
778 with open(rpzFilePath, 'w') as rpzZone:
779 rpzZone.write("""$ORIGIN zone.rpz.
780 @ 3600 IN SOA {soa}
781 secure.example.zone.rpz. 60 IN A 192.0.2.42
782 """.format(soa=cls._SOA))
783 super(PolicyEventFilterOnFollowUpTest, cls).generateRecursorConfig(confdir)
784
785 def testA(self):
786 expected = [
787 dns.rrset.from_text('policyeventfilter-followup.test.powerdns.com.', 15, dns.rdataclass.IN, 'CNAME', 'secure.example.'),
788 dns.rrset.from_text('secure.example.', 15, dns.rdataclass.IN, 'A', '192.0.2.17')
789 ]
790 query = dns.message.make_query('policyeventfilter-followup.test.powerdns.com.', 'A')
791
792 for method in ("sendUDPQuery", "sendTCPQuery"):
793 sender = getattr(self, method)
794 res = sender(query)
795
796 self.assertRcodeEqual(res, dns.rcode.NOERROR)
797 self.assertEqual(len(res.answer), 2)
798 self.assertEqual(len(res.authority), 0)
799 self.assertResponseMatches(query, expected, res)
800
801 class PolicyEventFilterOnFollowUpWithNativeDNS64Test(RecursorTest):
802 """Tests the interaction between followup queries and native dns64
803 """
804
805 _confdir = 'policyeventfilter-followup-dns64'
806 _config_template = """
807 dns64-prefix=1234::/96
808 """
809 _lua_config_file = """
810 """
811 _lua_dns_script_file = """
812 function preresolve(dq)
813 dq:addAnswer(pdns.CNAME, "cname.secure.example.")
814 dq.followupFunction="followCNAMERecords"
815 dq.rcode = pdns.NOERROR
816 return true
817 end
818
819 """
820
821 def testAAAA(self):
822 expected = [
823 dns.rrset.from_text('mx1.secure.example.', 15, dns.rdataclass.IN, 'CNAME', 'cname.secure.example.'),
824 dns.rrset.from_text('cname.secure.example.', 15, dns.rdataclass.IN, 'CNAME', ' host1.secure.example.'),
825 dns.rrset.from_text('host1.secure.example.', 15, dns.rdataclass.IN, 'AAAA', '1234::c000:202')
826 ]
827 query = dns.message.make_query('mx1.secure.example', 'AAAA')
828
829 for method in ("sendUDPQuery", "sendTCPQuery"):
830 sender = getattr(self, method)
831 res = sender(query)
832 print(res)
833 self.assertRcodeEqual(res, dns.rcode.NOERROR)
834 self.assertEqual(len(res.answer), 3)
835 self.assertEqual(len(res.authority), 0)
836 self.assertResponseMatches(query, expected, res)
837
838 class LuaPostResolveFFITest(RecursorTest):
839 """Tests postresolve_ffi interface"""
840
841 _confdir = 'LuaPostResolveFFITest'
842 _config_template = """
843 """
844 _lua_dns_script_file = """
845 local ffi = require("ffi")
846
847 ffi.cdef[[
848 typedef struct pdns_postresolve_ffi_handle pdns_postresolve_ffi_handle_t;
849
850 typedef enum
851 {
852 pdns_record_place_answer = 1,
853 pdns_record_place_authority = 2,
854 pdns_record_place_additional = 3
855 } pdns_record_place_t;
856
857 typedef enum
858 {
859 pdns_policy_kind_noaction = 0,
860 pdns_policy_kind_drop = 1,
861 pdns_policy_kind_nxdomain = 2,
862 pdns_policy_kind_nodata = 3,
863 pdns_policy_kind_truncate = 4,
864 pdns_policy_kind_custom = 5
865 } pdns_policy_kind_t;
866
867 typedef struct pdns_ffi_record {
868 const char* name;
869 size_t name_len;
870 const char* content;
871 size_t content_len;
872 uint32_t ttl;
873 pdns_record_place_t place;
874 uint16_t type;
875 } pdns_ffi_record_t;
876
877 const char* pdns_postresolve_ffi_handle_get_qname(pdns_postresolve_ffi_handle_t* ref);
878 const char* pdns_postresolve_ffi_handle_get_qname_raw(pdns_postresolve_ffi_handle_t* ref, const char** name, size_t* len);
879 uint16_t pdns_postresolve_ffi_handle_get_qtype(const pdns_postresolve_ffi_handle_t* ref);
880 uint16_t pdns_postresolve_ffi_handle_get_rcode(const pdns_postresolve_ffi_handle_t* ref);
881 void pdns_postresolve_ffi_handle_set_rcode(const pdns_postresolve_ffi_handle_t* ref, uint16_t rcode);
882 void pdns_postresolve_ffi_handle_set_appliedpolicy_kind(pdns_postresolve_ffi_handle_t* ref, pdns_policy_kind_t kind);
883 bool pdns_postresolve_ffi_handle_get_record(pdns_postresolve_ffi_handle_t* ref, unsigned int i, pdns_ffi_record_t *record, bool raw);
884 bool pdns_postresolve_ffi_handle_set_record(pdns_postresolve_ffi_handle_t* ref, unsigned int i, const char* content, size_t contentLen, bool raw);
885 void pdns_postresolve_ffi_handle_clear_records(pdns_postresolve_ffi_handle_t* ref);
886 bool pdns_postresolve_ffi_handle_add_record(pdns_postresolve_ffi_handle_t* ref, const char* name, uint16_t type, uint32_t ttl, const char* content, size_t contentLen, pdns_record_place_t place, bool raw);
887 const char* pdns_postresolve_ffi_handle_get_authip(pdns_postresolve_ffi_handle_t* ref);
888 void pdns_postresolve_ffi_handle_get_authip_raw(pdns_postresolve_ffi_handle_t* ref, const void** addr, size_t* addrSize);
889 ]]
890
891
892 function tohex(str)
893 return (str:gsub('.', function (c) return string.format('%02X', string.byte(c)) end))
894 end
895 function toA(str)
896 return (str:gsub('.', function (c) return string.format('%d.', string.byte(c)) end))
897 end
898
899 function postresolve_ffi(ref)
900 local qname = ffi.string(ffi.C.pdns_postresolve_ffi_handle_get_qname(ref))
901 local qtype = ffi.C.pdns_postresolve_ffi_handle_get_qtype(ref)
902
903 if qname == "example" and qtype == pdns.SOA
904 then
905 local addr = ffi.string(ffi.C.pdns_postresolve_ffi_handle_get_authip(ref))
906 if string.sub(addr, -3) ~= ".10" and string.sub(addr, -3) ~= ".18"
907 then
908 -- signal error by clearing all
909 ffi.C.pdns_postresolve_ffi_handle_clear_records(ref)
910 end
911 local qaddr = ffi.new("const char *[1]")
912 local qlen = ffi.new("size_t [1]")
913 ffi.C.pdns_postresolve_ffi_handle_get_qname_raw(ref, qaddr, qlen)
914 local q = ffi.string(qaddr[0], qlen[0])
915 if tohex(q) ~= "076578616D706C6500"
916 then
917 -- pdnslog("Error "..tohex(q))
918 -- signal error by clearing all
919 ffi.C.pdns_postresolve_ffi_handle_clear_records(ref)
920 end
921 -- as a bonus check from which auth the data came
922 local addr = ffi.new("const void *[1]")
923 local len = ffi.new("size_t [1]")
924 ffi.C.pdns_postresolve_ffi_handle_get_authip_raw(ref, addr, len)
925 local a = ffi.string(addr[0], len[0])
926 if string.byte(a, 4) ~= 10 and string.byte(a,4) ~= 18
927 then
928 -- signal error by clearing all
929 ffi.C.pdns_postresolve_ffi_handle_clear_records(ref)
930 end
931 ffi.C.pdns_postresolve_ffi_handle_set_appliedpolicy_kind(ref, "pdns_policy_kind_noaction")
932 return true
933 end
934 if qname == "example" and qtype == pdns.TXT
935 then
936 ffi.C.pdns_postresolve_ffi_handle_set_appliedpolicy_kind(ref, "pdns_policy_kind_drop")
937 return true
938 end
939 if qname == "ns1.example" and qtype == pdns.A
940 then
941 ffi.C.pdns_postresolve_ffi_handle_set_appliedpolicy_kind(ref, "pdns_policy_kind_nxdomain")
942 return true
943 end
944 if qname == "ns1.example" and qtype == pdns.AAAA
945 then
946 ffi.C.pdns_postresolve_ffi_handle_set_appliedpolicy_kind(ref, "pdns_policy_kind_nodata")
947 return true
948 end
949 if qname == "ns2.example" and qtype == pdns.A
950 then
951 ffi.C.pdns_postresolve_ffi_handle_set_appliedpolicy_kind(ref, "pdns_policy_kind_truncate")
952 return true
953 end
954
955 if qname ~= "postresolve_ffi.example" or (qtype ~= pdns.A and qtype ~= pdns.AAAA)
956 then
957 return false
958 end
959
960 local record = ffi.new("pdns_ffi_record_t")
961 local i = 0
962
963 while ffi.C.pdns_postresolve_ffi_handle_get_record(ref, i, record, false)
964 do
965 local content = ffi.string(record.content, record.content_len)
966 local name = ffi.string(record.name)
967 if name ~= "postresolve_ffi.example"
968 then
969 return false
970 end
971 if record.type == pdns.A and content == "1.2.3.4"
972 then
973 ffi.C.pdns_postresolve_ffi_handle_set_record(ref, i, "0.1.2.3", 7, false);
974 ffi.C.pdns_postresolve_ffi_handle_add_record(ref, "add."..name, pdns.A, record.ttl, '4.5.6.7', 7, "pdns_record_place_additional", false);
975 elseif record.type == pdns.AAAA and content == "::1"
976 then
977 ffi.C.pdns_postresolve_ffi_handle_set_record(ref, i, "\\0\\1\\2\\3\\4\\5\\6\\7\\8\\9\\10\\11\\12\\13\\14\\15", 16, true);
978 end
979 i = i + 1
980 end
981 -- loop again using raw
982 i = 0
983 while ffi.C.pdns_postresolve_ffi_handle_get_record(ref, i, record, true)
984 do
985 local content = ffi.string(record.content, record.content_len)
986 local name = ffi.string(record.name, record.name_len)
987 --pdnslog("R "..tohex(name))
988 if tohex(name) ~= "0F706F73747265736F6C76655F666669076578616D706C6500"
989 then
990 ffi.C.pdns_postresolve_ffi_handle_clear_records(ref)
991 end
992 i = i + 1
993 end
994 return true
995 end
996 """
997
998 def testNOACTION(self):
999 """ postresolve_ffi: test that we can do a NOACTION for a name and type combo"""
1000 query = dns.message.make_query('example', 'SOA')
1001 res = self.sendUDPQuery(query)
1002 self.assertRcodeEqual(res, dns.rcode.NOERROR)
1003 self.assertEqual(len(res.answer), 1)
1004
1005 def testDROP(self):
1006 """ postresolve_ffi: test that we can do a DROP for a name and type combo"""
1007 query = dns.message.make_query('example', 'TXT')
1008 res = self.sendUDPQuery(query)
1009 self.assertEqual(res, None)
1010
1011 def testNXDOMAIN(self):
1012 """ postresolve_ffi: test that we can return a NXDOMAIN for a name and type combo"""
1013 query = dns.message.make_query('ns1.example', 'A')
1014 res = self.sendUDPQuery(query)
1015 self.assertRcodeEqual(res, dns.rcode.NXDOMAIN)
1016 self.assertEqual(len(res.answer), 0)
1017
1018 def testNODATA(self):
1019 """ postresolve_ffi: test that we can return a NODATA for a name and type combo"""
1020 query = dns.message.make_query('ns1.example', 'AAAA')
1021 res = self.sendUDPQuery(query)
1022 self.assertRcodeEqual(res, dns.rcode.NOERROR)
1023 self.assertEqual(len(res.answer), 0)
1024
1025 def testTRUNCATE(self):
1026 """ postresolve_ffi: test that we can return a truncated for a name and type combo"""
1027 query = dns.message.make_query('ns2.example', 'A')
1028 res = self.sendUDPQuery(query)
1029 self.assertRcodeEqual(res, dns.rcode.NOERROR)
1030 self.assertEqual(len(res.answer), 0)
1031 self.assertMessageHasFlags(res, ['QR', 'TC', 'RD', 'RA'])
1032
1033
1034 def testModifyA(self):
1035 """postresolve_ffi: test that we can modify A answers"""
1036 expectedAnswerRecords = [
1037 dns.rrset.from_text('postresolve_ffi.example.', 60, dns.rdataclass.IN, 'A', '0.1.2.3', '1.2.3.5'),
1038 ]
1039 expectedAdditionalRecords = [
1040 dns.rrset.from_text('add.postresolve_ffi.example.', 60, dns.rdataclass.IN, 'A', '4.5.6.7'),
1041 ]
1042
1043 query = dns.message.make_query('postresolve_ffi.example', 'A')
1044 res = self.sendUDPQuery(query)
1045 self.assertRcodeEqual(res, dns.rcode.NOERROR)
1046 self.assertEqual(len(res.answer), 1)
1047 self.assertEqual(len(res.authority), 0)
1048 self.assertEqual(len(res.additional), 1)
1049 self.assertEqual(res.answer, expectedAnswerRecords)
1050 self.assertEqual(res.additional, expectedAdditionalRecords)
1051
1052 def testModifyAAAA(self):
1053 """postresolve_ffi: test that we can modify AAAA answers"""
1054 expectedAnswerRecords = [
1055 dns.rrset.from_text('postresolve_ffi.example.', 60, dns.rdataclass.IN, 'AAAA', '1:203:405:607:809:a0b:c0d:e0f', '::2'),
1056 ]
1057 query = dns.message.make_query('postresolve_ffi.example', 'AAAA')
1058 res = self.sendUDPQuery(query)
1059 self.assertRcodeEqual(res, dns.rcode.NOERROR)
1060 self.assertEqual(len(res.answer), 1)
1061 self.assertEqual(len(res.authority), 0)
1062 self.assertEqual(len(res.additional), 0)
1063 self.assertEqual(res.answer, expectedAnswerRecords)