]>
Commit | Line | Data |
---|---|---|
18e0e4df RG |
1 | import clientsubnetoption |
2 | import cookiesoption | |
3 | import dns | |
4 | import os | |
d19bcbf0 RG |
5 | import threading |
6 | import time | |
7 | ||
8 | from twisted.internet.protocol import DatagramProtocol | |
9 | from twisted.internet import reactor | |
18e0e4df RG |
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 | if dq.qtype == pdns.A then | |
103 | dq:addAnswer(pdns.A, '192.0.2.1') | |
104 | elseif dq.qtype == pdns.AAAA then | |
105 | dq:addAnswer(pdns.AAAA, '2001:db8::1') | |
106 | end | |
107 | ||
108 | return true | |
109 | end | |
110 | """ | |
111 | ||
112 | @classmethod | |
113 | def setUpClass(cls): | |
114 | ||
115 | cls.setUpSockets() | |
116 | confdir = os.path.join('configs', cls._confdir) | |
117 | cls.createConfigDir(confdir) | |
118 | cls.generateRecursorConfig(confdir) | |
119 | cls.startRecursor(confdir, cls._recursorPort) | |
120 | ||
121 | @classmethod | |
122 | def tearDownClass(cls): | |
123 | cls.tearDownRecursor() | |
124 | ||
18e0e4df RG |
125 | def testA(self): |
126 | name = 'gettag.lua.' | |
127 | expected = [ | |
128 | dns.rrset.from_text(name, 0, dns.rdataclass.IN, 'A', '192.0.2.1'), | |
129 | dns.rrset.from_text_list(name, 0, dns.rdataclass.IN, 'TXT', [ name, 'gettag-qtype-1']) | |
130 | ] | |
131 | query = dns.message.make_query(name, 'A', want_dnssec=True) | |
132 | query.flags |= dns.flags.CD | |
133 | res = self.sendUDPQuery(query) | |
134 | self.assertResponseMatches(query, expected, res) | |
135 | ||
136 | def testTCPA(self): | |
137 | name = 'gettag-tcpa.lua.' | |
138 | expected = [ | |
139 | dns.rrset.from_text(name, 0, dns.rdataclass.IN, 'A', '192.0.2.1'), | |
140 | dns.rrset.from_text_list(name, 0, dns.rdataclass.IN, 'TXT', [ name, 'gettag-qtype-1', 'gettag-tcp']) | |
141 | ] | |
142 | query = dns.message.make_query(name, 'A', want_dnssec=True) | |
143 | query.flags |= dns.flags.CD | |
144 | res = self.sendTCPQuery(query) | |
145 | self.assertResponseMatches(query, expected, res) | |
146 | ||
147 | def testAAAA(self): | |
148 | name = 'gettag-aaaa.lua.' | |
149 | expected = [ | |
150 | dns.rrset.from_text(name, 0, dns.rdataclass.IN, 'AAAA', '2001:db8::1'), | |
151 | dns.rrset.from_text_list(name, 0, dns.rdataclass.IN, 'TXT', [ name, 'gettag-qtype-28']) | |
152 | ] | |
153 | query = dns.message.make_query(name, 'AAAA', want_dnssec=True) | |
154 | query.flags |= dns.flags.CD | |
155 | res = self.sendUDPQuery(query) | |
156 | self.assertResponseMatches(query, expected, res) | |
157 | ||
cf12aaef RG |
158 | def testAAAA(self): |
159 | name = 'gettag-tcpaaaa.lua.' | |
160 | expected = [ | |
161 | dns.rrset.from_text(name, 0, dns.rdataclass.IN, 'AAAA', '2001:db8::1'), | |
162 | dns.rrset.from_text_list(name, 0, dns.rdataclass.IN, 'TXT', [ name, 'gettag-qtype-28', 'gettag-tcp']) | |
163 | ] | |
164 | query = dns.message.make_query(name, 'AAAA', want_dnssec=True) | |
165 | query.flags |= dns.flags.CD | |
166 | res = self.sendTCPQuery(query) | |
167 | self.assertResponseMatches(query, expected, res) | |
168 | ||
18e0e4df RG |
169 | def testSubnet(self): |
170 | name = 'gettag-subnet.lua.' | |
171 | subnet = '192.0.2.255' | |
172 | subnetMask = 32 | |
173 | ecso = clientsubnetoption.ClientSubnetOption(subnet, subnetMask) | |
174 | expected = [ | |
175 | dns.rrset.from_text(name, 0, dns.rdataclass.IN, 'A', '192.0.2.1'), | |
176 | dns.rrset.from_text_list(name, 0, dns.rdataclass.IN, 'TXT', [name, 'gettag-qtype-1', 'edns-subnet-' + subnet + '/' + str(subnetMask), | |
177 | 'ednsoption-8-count-1', 'ednsoption-8-total-len-8']), | |
178 | ] | |
179 | query = dns.message.make_query(name, 'A', want_dnssec=True, options=[ecso]) | |
180 | query.flags |= dns.flags.CD | |
181 | res = self.sendUDPQuery(query) | |
182 | self.assertResponseMatches(query, expected, res) | |
183 | ||
184 | def testEDNSOptions(self): | |
185 | name = 'gettag-ednsoptions.lua.' | |
186 | subnet = '192.0.2.255' | |
187 | subnetMask = 32 | |
188 | ecso = clientsubnetoption.ClientSubnetOption(subnet, subnetMask) | |
189 | eco1 = cookiesoption.CookiesOption(b'deadbeef', b'deadbeef') | |
190 | eco2 = cookiesoption.CookiesOption(b'deadc0de', b'deadc0de') | |
191 | ||
192 | expected = [ | |
193 | dns.rrset.from_text(name, 0, dns.rdataclass.IN, 'A', '192.0.2.1'), | |
194 | dns.rrset.from_text_list(name, 0, dns.rdataclass.IN, 'TXT', [name, 'gettag-qtype-1', 'edns-subnet-' + subnet + '/' + str(subnetMask), | |
195 | 'ednsoption-10-count-2', 'ednsoption-10-total-len-32', | |
196 | 'ednsoption-8-count-1', 'ednsoption-8-total-len-8' | |
197 | ]), | |
198 | ] | |
199 | query = dns.message.make_query(name, 'A', want_dnssec=True, options=[eco1,ecso,eco2]) | |
200 | query.flags |= dns.flags.CD | |
201 | res = self.sendUDPQuery(query) | |
202 | self.assertResponseMatches(query, expected, res) | |
203 | ||
cf12aaef RG |
204 | class GettagRecursorDistributesQueriesTest(GettagRecursorTest): |
205 | _confdir = 'LuaGettagDistributes' | |
206 | _config_template = """ | |
207 | log-common-errors=yes | |
208 | gettag-needs-edns-options=yes | |
209 | pdns-distributes-queries=yes | |
210 | threads=2 | |
211 | """ | |
212 | ||
d19bcbf0 RG |
213 | hooksReactorRunning = False |
214 | ||
215 | class UDPHooksResponder(DatagramProtocol): | |
216 | ||
217 | def datagramReceived(self, datagram, address): | |
218 | request = dns.message.from_wire(datagram) | |
219 | ||
220 | response = dns.message.make_response(request) | |
221 | response.flags |= dns.flags.AA | |
222 | ||
223 | if request.question[0].name == dns.name.from_text('nxdomain.luahooks.example.'): | |
224 | soa = dns.rrset.from_text('luahooks.example.', 86400, dns.rdataclass.IN, 'SOA', 'ns.luahooks.example. hostmaster.luahooks.example. 1 3600 3600 3600 1') | |
225 | response.authority.append(soa) | |
226 | response.set_rcode(dns.rcode.NXDOMAIN) | |
227 | ||
228 | elif request.question[0].name == dns.name.from_text('nodata.luahooks.example.'): | |
229 | soa = dns.rrset.from_text('luahooks.example.', 86400, dns.rdataclass.IN, 'SOA', 'ns.luahooks.example. hostmaster.luahooks.example. 1 3600 3600 3600 1') | |
230 | response.authority.append(soa) | |
231 | ||
232 | elif request.question[0].name == dns.name.from_text('postresolve.luahooks.example.'): | |
233 | answer = dns.rrset.from_text('postresolve.luahooks.example.', 3600, dns.rdataclass.IN, 'A', '192.0.2.1') | |
234 | response.answer.append(answer) | |
235 | ||
236 | self.transport.write(response.to_wire(), address) | |
237 | ||
238 | class LuaHooksRecursorTest(RecursorTest): | |
239 | _confdir = 'LuaHooks' | |
240 | _config_template = """ | |
241 | forward-zones=luahooks.example=%s.23 | |
242 | log-common-errors=yes | |
243 | quiet=no | |
244 | """ % (os.environ['PREFIX']) | |
245 | _lua_dns_script_file = """ | |
246 | ||
247 | allowedips = newNMG() | |
248 | allowedips:addMask("%s.0/24") | |
249 | ||
250 | function ipfilter(remoteip, localip, dh) | |
251 | -- allow only 127.0.0.1 and AD=0 | |
252 | if allowedips:match(remoteip) and not dh:getAD() then | |
253 | return false | |
254 | end | |
255 | ||
256 | return true | |
257 | end | |
258 | ||
259 | function nodata(dq) | |
260 | if dq.qtype == pdns.AAAA and dq.qname == newDN("nodata.luahooks.example.") then | |
261 | dq:addAnswer(pdns.AAAA, "2001:DB8::1") | |
262 | return true | |
263 | end | |
264 | ||
265 | return false | |
266 | end | |
267 | ||
268 | function nxdomain(dq) | |
269 | if dq.qtype == pdns.A and dq.qname == newDN("nxdomain.luahooks.example.") then | |
270 | dq.rcode=0 | |
271 | dq:addAnswer(pdns.A, "192.0.2.1") | |
272 | return true | |
273 | end | |
274 | ||
275 | return false | |
276 | end | |
277 | ||
278 | function postresolve(dq) | |
279 | if dq.qtype == pdns.A and dq.qname == newDN("postresolve.luahooks.example.") then | |
280 | local records = dq:getRecords() | |
281 | for k,v in pairs(records) do | |
282 | if v.type == pdns.A and v:getContent() == "192.0.2.1" then | |
283 | v:changeContent("192.0.2.42") | |
284 | v.ttl=1 | |
285 | end | |
286 | end | |
287 | dq:setRecords(records) | |
288 | return true | |
289 | end | |
290 | ||
291 | return false | |
292 | end | |
293 | ||
294 | function preoutquery(dq) | |
295 | if dq.remoteaddr:equal(newCA("%s.23")) and dq.qname == newDN("preout.luahooks.example.") and dq.qtype == pdns.A then | |
296 | dq.rcode = -3 -- "kill" | |
297 | return true | |
298 | end | |
299 | ||
300 | return false | |
301 | end | |
302 | ||
303 | """ % (os.environ['PREFIX'], os.environ['PREFIX']) | |
304 | ||
305 | @classmethod | |
306 | def startResponders(cls): | |
307 | global hooksReactorRunning | |
308 | print("Launching responders..") | |
309 | ||
310 | address = cls._PREFIX + '.23' | |
311 | port = 53 | |
312 | ||
313 | if not hooksReactorRunning: | |
314 | reactor.listenUDP(port, UDPHooksResponder(), interface=address) | |
315 | hooksReactorRunning = True | |
316 | ||
317 | if not reactor.running: | |
318 | cls._UDPResponder = threading.Thread(name='UDP Hooks Responder', target=reactor.run, args=(False,)) | |
319 | cls._UDPResponder.setDaemon(True) | |
320 | cls._UDPResponder.start() | |
321 | ||
322 | @classmethod | |
323 | def setUpClass(cls): | |
324 | cls.setUpSockets() | |
325 | ||
326 | cls.startResponders() | |
327 | ||
328 | confdir = os.path.join('configs', cls._confdir) | |
329 | cls.createConfigDir(confdir) | |
330 | ||
331 | cls.generateRecursorConfig(confdir) | |
332 | cls.startRecursor(confdir, cls._recursorPort) | |
333 | ||
334 | print("Launching tests..") | |
335 | ||
336 | @classmethod | |
337 | def tearDownClass(cls): | |
338 | cls.tearDownRecursor() | |
339 | ||
340 | def testNoData(self): | |
341 | expected = dns.rrset.from_text('nodata.luahooks.example.', 3600, dns.rdataclass.IN, 'AAAA', '2001:DB8::1') | |
342 | query = dns.message.make_query('nodata.luahooks.example.', 'AAAA', 'IN') | |
d19bcbf0 | 343 | |
cf12aaef RG |
344 | for method in ("sendUDPQuery", "sendTCPQuery"): |
345 | sender = getattr(self, method) | |
346 | res = sender(query) | |
347 | self.assertRcodeEqual(res, dns.rcode.NOERROR) | |
348 | self.assertRRsetInAnswer(res, expected) | |
d19bcbf0 RG |
349 | |
350 | def testVanillaNXD(self): | |
351 | #expected = dns.rrset.from_text('nxdomain.luahooks.example.', 3600, dns.rdataclass.IN, 'A', '192.0.2.1') | |
352 | query = dns.message.make_query('nxdomain.luahooks.example.', 'AAAA', 'IN') | |
d19bcbf0 | 353 | |
cf12aaef RG |
354 | for method in ("sendUDPQuery", "sendTCPQuery"): |
355 | sender = getattr(self, method) | |
356 | res = sender(query) | |
357 | self.assertRcodeEqual(res, dns.rcode.NXDOMAIN) | |
d19bcbf0 RG |
358 | |
359 | def testHookedNXD(self): | |
360 | expected = dns.rrset.from_text('nxdomain.luahooks.example.', 3600, dns.rdataclass.IN, 'A', '192.0.2.1') | |
361 | query = dns.message.make_query('nxdomain.luahooks.example.', 'A', 'IN') | |
d19bcbf0 | 362 | |
cf12aaef RG |
363 | for method in ("sendUDPQuery", "sendTCPQuery"): |
364 | sender = getattr(self, method) | |
365 | res = sender(query) | |
366 | self.assertRcodeEqual(res, dns.rcode.NOERROR) | |
367 | self.assertRRsetInAnswer(res, expected) | |
d19bcbf0 RG |
368 | |
369 | def testPostResolve(self): | |
370 | expected = dns.rrset.from_text('postresolve.luahooks.example.', 1, dns.rdataclass.IN, 'A', '192.0.2.42') | |
371 | query = dns.message.make_query('postresolve.luahooks.example.', 'A', 'IN') | |
d19bcbf0 | 372 | |
cf12aaef RG |
373 | for method in ("sendUDPQuery", "sendTCPQuery"): |
374 | sender = getattr(self, method) | |
375 | res = sender(query) | |
376 | self.assertRcodeEqual(res, dns.rcode.NOERROR) | |
377 | self.assertRRsetInAnswer(res, expected) | |
378 | self.assertEqual(res.answer[0].ttl, 1) | |
d19bcbf0 RG |
379 | |
380 | def testIPFilterHeader(self): | |
381 | query = dns.message.make_query('ipfiler.luahooks.example.', 'A', 'IN') | |
382 | query.flags |= dns.flags.AD | |
cf12aaef RG |
383 | |
384 | for method in ("sendUDPQuery", "sendTCPQuery"): | |
385 | sender = getattr(self, method) | |
386 | res = sender(query) | |
387 | self.assertEqual(res, None) | |
d19bcbf0 RG |
388 | |
389 | def testPreOutInterceptedQuery(self): | |
390 | query = dns.message.make_query('preout.luahooks.example.', 'A', 'IN') | |
cf12aaef RG |
391 | |
392 | for method in ("sendUDPQuery", "sendTCPQuery"): | |
393 | sender = getattr(self, method) | |
394 | res = sender(query) | |
395 | self.assertRcodeEqual(res, dns.rcode.SERVFAIL) | |
d19bcbf0 RG |
396 | |
397 | def testPreOutNotInterceptedQuery(self): | |
398 | query = dns.message.make_query('preout.luahooks.example.', 'AAAA', 'IN') | |
f3f19ebe | 399 | |
cf12aaef RG |
400 | for method in ("sendUDPQuery", "sendTCPQuery"): |
401 | sender = getattr(self, method) | |
402 | res = sender(query) | |
403 | self.assertRcodeEqual(res, dns.rcode.NOERROR) | |
404 | ||
405 | class LuaHooksRecursorDistributesTest(LuaHooksRecursorTest): | |
406 | _confdir = 'LuaHooksDistributes' | |
407 | _config_template = """ | |
408 | forward-zones=luahooks.example=%s.23 | |
409 | log-common-errors=yes | |
410 | pdns-distributes-queries=yes | |
411 | threads=2 | |
412 | quiet=no | |
413 | """ % (os.environ['PREFIX']) | |
f3f19ebe PL |
414 | |
415 | class DNS64Test(RecursorTest): | |
416 | """Tests the dq.followupAction("getFakeAAAARecords")""" | |
417 | ||
418 | _confdir = 'dns64' | |
419 | _config_template = """ | |
420 | """ | |
421 | _lua_dns_script_file = """ | |
422 | prefix = "2001:DB8:64::" | |
423 | ||
424 | function nodata (dq) | |
425 | if dq.qtype ~= pdns.AAAA then | |
426 | return false | |
427 | end -- only AAAA records | |
428 | ||
429 | -- don't fake AAAA records if DNSSEC validation failed | |
430 | if dq.validationState == pdns.validationstates.Bogus then | |
431 | return false | |
432 | end | |
433 | ||
434 | dq.followupFunction = "getFakeAAAARecords" | |
435 | dq.followupPrefix = prefix | |
436 | dq.followupName = dq.qname | |
437 | return true | |
438 | end | |
439 | """ | |
440 | ||
441 | def testAtoAAAA(self): | |
442 | expected = [ | |
443 | dns.rrset.from_text('ns.secure.example.', 15, dns.rdataclass.IN, 'AAAA', '2001:db8:64::7f00:9') | |
444 | ] | |
445 | query = dns.message.make_query('ns.secure.example', 'AAAA') | |
446 | ||
cf12aaef RG |
447 | for method in ("sendUDPQuery", "sendTCPQuery"): |
448 | sender = getattr(self, method) | |
449 | res = sender(query) | |
f3f19ebe | 450 | |
cf12aaef RG |
451 | self.assertRcodeEqual(res, dns.rcode.NOERROR) |
452 | self.assertEqual(len(res.answer), 1) | |
453 | self.assertEqual(len(res.authority), 0) | |
454 | self.assertResponseMatches(query, expected, res) | |
f3f19ebe PL |
455 | |
456 | def testAtoCNAMEtoAAAA(self): | |
457 | expected = [ | |
458 | dns.rrset.from_text('cname-to-insecure.secure.example.', 3600, dns.rdataclass.IN, 'CNAME', 'node1.insecure.example.'), | |
459 | dns.rrset.from_text('node1.insecure.example.', 3600, dns.rdataclass.IN, 'AAAA', '2001:db8:64::c000:206') | |
460 | ] | |
461 | query = dns.message.make_query('cname-to-insecure.secure.example.', 'AAAA') | |
462 | ||
cf12aaef RG |
463 | for method in ("sendUDPQuery", "sendTCPQuery"): |
464 | sender = getattr(self, method) | |
465 | res = sender(query) | |
f3f19ebe | 466 | |
cf12aaef RG |
467 | self.assertRcodeEqual(res, dns.rcode.NOERROR) |
468 | self.assertEqual(len(res.answer), 2) | |
469 | self.assertEqual(len(res.authority), 0) | |
470 | self.assertResponseMatches(query, expected, res) | |
2c419831 PL |
471 | |
472 | ||
473 | class PDNSRandomTest(RecursorTest): | |
474 | """Tests if pdnsrandom works""" | |
475 | ||
476 | _confdir = 'pdnsrandom' | |
477 | _config_template = """ | |
478 | """ | |
479 | _lua_dns_script_file = """ | |
480 | function preresolve (dq) | |
481 | dq.rcode = pdns.NOERROR | |
482 | dq:addAnswer(pdns.TXT, pdnsrandom()) | |
483 | return true | |
484 | end | |
485 | """ | |
486 | ||
487 | def testRandom(self): | |
488 | query = dns.message.make_query('whatever.example.', 'TXT') | |
489 | ||
490 | ans = set() | |
491 | ||
492 | ret = self.sendUDPQuery(query) | |
493 | ans.add(ret.answer[0]) | |
494 | ret = self.sendUDPQuery(query) | |
495 | ans.add(ret.answer[0]) | |
496 | ||
497 | self.assertEqual(len(ans), 2) |