]>
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 | ||
125 | def assertResponseMatches(self, query, expectedRRs, response): | |
126 | expectedResponse = dns.message.make_response(query) | |
127 | ||
128 | if query.flags & dns.flags.RD: | |
129 | expectedResponse.flags |= dns.flags.RA | |
130 | if query.flags & dns.flags.CD: | |
131 | expectedResponse.flags |= dns.flags.CD | |
132 | ||
133 | expectedResponse.answer = expectedRRs | |
134 | print(expectedResponse) | |
135 | print(response) | |
136 | self.assertEquals(response, expectedResponse) | |
137 | ||
138 | def testA(self): | |
139 | name = 'gettag.lua.' | |
140 | expected = [ | |
141 | dns.rrset.from_text(name, 0, dns.rdataclass.IN, 'A', '192.0.2.1'), | |
142 | dns.rrset.from_text_list(name, 0, dns.rdataclass.IN, 'TXT', [ name, 'gettag-qtype-1']) | |
143 | ] | |
144 | query = dns.message.make_query(name, 'A', want_dnssec=True) | |
145 | query.flags |= dns.flags.CD | |
146 | res = self.sendUDPQuery(query) | |
147 | self.assertResponseMatches(query, expected, res) | |
148 | ||
149 | def testTCPA(self): | |
150 | name = 'gettag-tcpa.lua.' | |
151 | expected = [ | |
152 | dns.rrset.from_text(name, 0, dns.rdataclass.IN, 'A', '192.0.2.1'), | |
153 | dns.rrset.from_text_list(name, 0, dns.rdataclass.IN, 'TXT', [ name, 'gettag-qtype-1', 'gettag-tcp']) | |
154 | ] | |
155 | query = dns.message.make_query(name, 'A', want_dnssec=True) | |
156 | query.flags |= dns.flags.CD | |
157 | res = self.sendTCPQuery(query) | |
158 | self.assertResponseMatches(query, expected, res) | |
159 | ||
160 | def testAAAA(self): | |
161 | name = 'gettag-aaaa.lua.' | |
162 | expected = [ | |
163 | dns.rrset.from_text(name, 0, dns.rdataclass.IN, 'AAAA', '2001:db8::1'), | |
164 | dns.rrset.from_text_list(name, 0, dns.rdataclass.IN, 'TXT', [ name, 'gettag-qtype-28']) | |
165 | ] | |
166 | query = dns.message.make_query(name, 'AAAA', want_dnssec=True) | |
167 | query.flags |= dns.flags.CD | |
168 | res = self.sendUDPQuery(query) | |
169 | self.assertResponseMatches(query, expected, res) | |
170 | ||
171 | def testSubnet(self): | |
172 | name = 'gettag-subnet.lua.' | |
173 | subnet = '192.0.2.255' | |
174 | subnetMask = 32 | |
175 | ecso = clientsubnetoption.ClientSubnetOption(subnet, subnetMask) | |
176 | expected = [ | |
177 | dns.rrset.from_text(name, 0, dns.rdataclass.IN, 'A', '192.0.2.1'), | |
178 | dns.rrset.from_text_list(name, 0, dns.rdataclass.IN, 'TXT', [name, 'gettag-qtype-1', 'edns-subnet-' + subnet + '/' + str(subnetMask), | |
179 | 'ednsoption-8-count-1', 'ednsoption-8-total-len-8']), | |
180 | ] | |
181 | query = dns.message.make_query(name, 'A', want_dnssec=True, options=[ecso]) | |
182 | query.flags |= dns.flags.CD | |
183 | res = self.sendUDPQuery(query) | |
184 | self.assertResponseMatches(query, expected, res) | |
185 | ||
186 | def testEDNSOptions(self): | |
187 | name = 'gettag-ednsoptions.lua.' | |
188 | subnet = '192.0.2.255' | |
189 | subnetMask = 32 | |
190 | ecso = clientsubnetoption.ClientSubnetOption(subnet, subnetMask) | |
191 | eco1 = cookiesoption.CookiesOption(b'deadbeef', b'deadbeef') | |
192 | eco2 = cookiesoption.CookiesOption(b'deadc0de', b'deadc0de') | |
193 | ||
194 | expected = [ | |
195 | dns.rrset.from_text(name, 0, dns.rdataclass.IN, 'A', '192.0.2.1'), | |
196 | dns.rrset.from_text_list(name, 0, dns.rdataclass.IN, 'TXT', [name, 'gettag-qtype-1', 'edns-subnet-' + subnet + '/' + str(subnetMask), | |
197 | 'ednsoption-10-count-2', 'ednsoption-10-total-len-32', | |
198 | 'ednsoption-8-count-1', 'ednsoption-8-total-len-8' | |
199 | ]), | |
200 | ] | |
201 | query = dns.message.make_query(name, 'A', want_dnssec=True, options=[eco1,ecso,eco2]) | |
202 | query.flags |= dns.flags.CD | |
203 | res = self.sendUDPQuery(query) | |
204 | self.assertResponseMatches(query, expected, res) | |
205 | ||
d19bcbf0 RG |
206 | hooksReactorRunning = False |
207 | ||
208 | class UDPHooksResponder(DatagramProtocol): | |
209 | ||
210 | def datagramReceived(self, datagram, address): | |
211 | request = dns.message.from_wire(datagram) | |
212 | ||
213 | response = dns.message.make_response(request) | |
214 | response.flags |= dns.flags.AA | |
215 | ||
216 | if request.question[0].name == dns.name.from_text('nxdomain.luahooks.example.'): | |
217 | soa = dns.rrset.from_text('luahooks.example.', 86400, dns.rdataclass.IN, 'SOA', 'ns.luahooks.example. hostmaster.luahooks.example. 1 3600 3600 3600 1') | |
218 | response.authority.append(soa) | |
219 | response.set_rcode(dns.rcode.NXDOMAIN) | |
220 | ||
221 | elif request.question[0].name == dns.name.from_text('nodata.luahooks.example.'): | |
222 | soa = dns.rrset.from_text('luahooks.example.', 86400, dns.rdataclass.IN, 'SOA', 'ns.luahooks.example. hostmaster.luahooks.example. 1 3600 3600 3600 1') | |
223 | response.authority.append(soa) | |
224 | ||
225 | elif request.question[0].name == dns.name.from_text('postresolve.luahooks.example.'): | |
226 | answer = dns.rrset.from_text('postresolve.luahooks.example.', 3600, dns.rdataclass.IN, 'A', '192.0.2.1') | |
227 | response.answer.append(answer) | |
228 | ||
229 | self.transport.write(response.to_wire(), address) | |
230 | ||
231 | class LuaHooksRecursorTest(RecursorTest): | |
232 | _confdir = 'LuaHooks' | |
233 | _config_template = """ | |
234 | forward-zones=luahooks.example=%s.23 | |
235 | log-common-errors=yes | |
236 | quiet=no | |
237 | """ % (os.environ['PREFIX']) | |
238 | _lua_dns_script_file = """ | |
239 | ||
240 | allowedips = newNMG() | |
241 | allowedips:addMask("%s.0/24") | |
242 | ||
243 | function ipfilter(remoteip, localip, dh) | |
244 | -- allow only 127.0.0.1 and AD=0 | |
245 | if allowedips:match(remoteip) and not dh:getAD() then | |
246 | return false | |
247 | end | |
248 | ||
249 | return true | |
250 | end | |
251 | ||
252 | function nodata(dq) | |
253 | if dq.qtype == pdns.AAAA and dq.qname == newDN("nodata.luahooks.example.") then | |
254 | dq:addAnswer(pdns.AAAA, "2001:DB8::1") | |
255 | return true | |
256 | end | |
257 | ||
258 | return false | |
259 | end | |
260 | ||
261 | function nxdomain(dq) | |
262 | if dq.qtype == pdns.A and dq.qname == newDN("nxdomain.luahooks.example.") then | |
263 | dq.rcode=0 | |
264 | dq:addAnswer(pdns.A, "192.0.2.1") | |
265 | return true | |
266 | end | |
267 | ||
268 | return false | |
269 | end | |
270 | ||
271 | function postresolve(dq) | |
272 | if dq.qtype == pdns.A and dq.qname == newDN("postresolve.luahooks.example.") then | |
273 | local records = dq:getRecords() | |
274 | for k,v in pairs(records) do | |
275 | if v.type == pdns.A and v:getContent() == "192.0.2.1" then | |
276 | v:changeContent("192.0.2.42") | |
277 | v.ttl=1 | |
278 | end | |
279 | end | |
280 | dq:setRecords(records) | |
281 | return true | |
282 | end | |
283 | ||
284 | return false | |
285 | end | |
286 | ||
287 | function preoutquery(dq) | |
288 | if dq.remoteaddr:equal(newCA("%s.23")) and dq.qname == newDN("preout.luahooks.example.") and dq.qtype == pdns.A then | |
289 | dq.rcode = -3 -- "kill" | |
290 | return true | |
291 | end | |
292 | ||
293 | return false | |
294 | end | |
295 | ||
296 | """ % (os.environ['PREFIX'], os.environ['PREFIX']) | |
297 | ||
298 | @classmethod | |
299 | def startResponders(cls): | |
300 | global hooksReactorRunning | |
301 | print("Launching responders..") | |
302 | ||
303 | address = cls._PREFIX + '.23' | |
304 | port = 53 | |
305 | ||
306 | if not hooksReactorRunning: | |
307 | reactor.listenUDP(port, UDPHooksResponder(), interface=address) | |
308 | hooksReactorRunning = True | |
309 | ||
310 | if not reactor.running: | |
311 | cls._UDPResponder = threading.Thread(name='UDP Hooks Responder', target=reactor.run, args=(False,)) | |
312 | cls._UDPResponder.setDaemon(True) | |
313 | cls._UDPResponder.start() | |
314 | ||
315 | @classmethod | |
316 | def setUpClass(cls): | |
317 | cls.setUpSockets() | |
318 | ||
319 | cls.startResponders() | |
320 | ||
321 | confdir = os.path.join('configs', cls._confdir) | |
322 | cls.createConfigDir(confdir) | |
323 | ||
324 | cls.generateRecursorConfig(confdir) | |
325 | cls.startRecursor(confdir, cls._recursorPort) | |
326 | ||
327 | print("Launching tests..") | |
328 | ||
329 | @classmethod | |
330 | def tearDownClass(cls): | |
331 | cls.tearDownRecursor() | |
332 | ||
333 | def testNoData(self): | |
334 | expected = dns.rrset.from_text('nodata.luahooks.example.', 3600, dns.rdataclass.IN, 'AAAA', '2001:DB8::1') | |
335 | query = dns.message.make_query('nodata.luahooks.example.', 'AAAA', 'IN') | |
336 | res = self.sendUDPQuery(query) | |
337 | ||
338 | self.assertRcodeEqual(res, dns.rcode.NOERROR) | |
339 | self.assertRRsetInAnswer(res, expected) | |
340 | ||
341 | def testVanillaNXD(self): | |
342 | #expected = dns.rrset.from_text('nxdomain.luahooks.example.', 3600, dns.rdataclass.IN, 'A', '192.0.2.1') | |
343 | query = dns.message.make_query('nxdomain.luahooks.example.', 'AAAA', 'IN') | |
344 | res = self.sendUDPQuery(query) | |
345 | ||
346 | self.assertRcodeEqual(res, dns.rcode.NXDOMAIN) | |
347 | ||
348 | def testHookedNXD(self): | |
349 | expected = dns.rrset.from_text('nxdomain.luahooks.example.', 3600, dns.rdataclass.IN, 'A', '192.0.2.1') | |
350 | query = dns.message.make_query('nxdomain.luahooks.example.', 'A', 'IN') | |
351 | res = self.sendUDPQuery(query) | |
352 | ||
353 | self.assertRcodeEqual(res, dns.rcode.NOERROR) | |
354 | self.assertRRsetInAnswer(res, expected) | |
355 | ||
356 | def testPostResolve(self): | |
357 | expected = dns.rrset.from_text('postresolve.luahooks.example.', 1, dns.rdataclass.IN, 'A', '192.0.2.42') | |
358 | query = dns.message.make_query('postresolve.luahooks.example.', 'A', 'IN') | |
359 | res = self.sendUDPQuery(query) | |
360 | ||
361 | self.assertRcodeEqual(res, dns.rcode.NOERROR) | |
362 | self.assertRRsetInAnswer(res, expected) | |
363 | self.assertEqual(res.answer[0].ttl, 1) | |
364 | ||
365 | def testIPFilterHeader(self): | |
366 | query = dns.message.make_query('ipfiler.luahooks.example.', 'A', 'IN') | |
367 | query.flags |= dns.flags.AD | |
368 | res = self.sendUDPQuery(query) | |
369 | self.assertEqual(res, None) | |
370 | ||
371 | def testPreOutInterceptedQuery(self): | |
372 | query = dns.message.make_query('preout.luahooks.example.', 'A', 'IN') | |
373 | res = self.sendUDPQuery(query) | |
374 | self.assertRcodeEqual(res, dns.rcode.SERVFAIL) | |
375 | ||
376 | def testPreOutNotInterceptedQuery(self): | |
377 | query = dns.message.make_query('preout.luahooks.example.', 'AAAA', 'IN') | |
378 | res = self.sendUDPQuery(query) | |
379 | self.assertRcodeEqual(res, dns.rcode.NOERROR) |