]> git.ipfire.org Git - thirdparty/pdns.git/blob - regression-tests.dnsdist/test_DOH.py
Merge pull request #12406 from mind04/auth-catalog-members
[thirdparty/pdns.git] / regression-tests.dnsdist / test_DOH.py
1 #!/usr/bin/env python
2
3 import dns
4 import os
5 import time
6 import unittest
7 import clientsubnetoption
8
9 from dnsdistdohtests import DNSDistDOHTest
10
11 import pycurl
12 from io import BytesIO
13
14 class TestDOH(DNSDistDOHTest):
15
16 _serverKey = 'server.key'
17 _serverCert = 'server.chain'
18 _serverName = 'tls.tests.dnsdist.org'
19 _caCert = 'ca.pem'
20 _dohServerPort = 8443
21 _customResponseHeader1 = 'access-control-allow-origin: *'
22 _customResponseHeader2 = 'user-agent: derp'
23 _dohBaseURL = ("https://%s:%d/" % (_serverName, _dohServerPort))
24 _config_template = """
25 newServer{address="127.0.0.1:%s"}
26
27 addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/", "/coffee", "/PowerDNS", "/PowerDNS2", "/PowerDNS-999" }, {customResponseHeaders={["access-control-allow-origin"]="*",["user-agent"]="derp",["UPPERCASE"]="VaLuE"}, keepIncomingHeaders=true})
28 dohFE = getDOHFrontend(0)
29 dohFE:setResponsesMap({newDOHResponseMapEntry('^/coffee$', 418, 'C0FFEE', {['FoO']='bar'})})
30
31 addAction("drop.doh.tests.powerdns.com.", DropAction())
32 addAction("refused.doh.tests.powerdns.com.", RCodeAction(DNSRCode.REFUSED))
33 addAction("spoof.doh.tests.powerdns.com.", SpoofAction("1.2.3.4"))
34 addAction(HTTPHeaderRule("X-PowerDNS", "^[a]{5}$"), SpoofAction("2.3.4.5"))
35 addAction(HTTPPathRule("/PowerDNS"), SpoofAction("3.4.5.6"))
36 addAction(HTTPPathRegexRule("^/PowerDNS-[0-9]"), SpoofAction("6.7.8.9"))
37 addAction("http-status-action.doh.tests.powerdns.com.", HTTPStatusAction(200, "Plaintext answer", "text/plain"))
38 addAction("http-status-action-redirect.doh.tests.powerdns.com.", HTTPStatusAction(307, "https://doh.powerdns.org"))
39
40 function dohHandler(dq)
41 if dq:getHTTPScheme() == 'https' and dq:getHTTPHost() == '%s:%d' and dq:getHTTPPath() == '/' and dq:getHTTPQueryString() == '' then
42 local foundct = false
43 for key,value in pairs(dq:getHTTPHeaders()) do
44 if key == 'content-type' and value == 'application/dns-message' then
45 foundct = true
46 break
47 end
48 end
49 if foundct then
50 dq:setHTTPResponse(200, 'It works!', 'text/plain')
51 dq.dh:setQR(true)
52 return DNSAction.HeaderModify
53 end
54 end
55 return DNSAction.None
56 end
57 addAction("http-lua.doh.tests.powerdns.com.", LuaAction(dohHandler))
58 """
59 _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey', '_serverName', '_dohServerPort']
60
61 def testDOHSimple(self):
62 """
63 DOH: Simple query
64 """
65 name = 'simple.doh.tests.powerdns.com.'
66 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
67 query.id = 0
68 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
69 expectedQuery.id = 0
70 response = dns.message.make_response(query)
71 rrset = dns.rrset.from_text(name,
72 3600,
73 dns.rdataclass.IN,
74 dns.rdatatype.A,
75 '127.0.0.1')
76 response.answer.append(rrset)
77
78 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert)
79 self.assertTrue(receivedQuery)
80 self.assertTrue(receivedResponse)
81 receivedQuery.id = expectedQuery.id
82 self.assertEqual(expectedQuery, receivedQuery)
83 self.assertTrue((self._customResponseHeader1) in self._response_headers.decode())
84 self.assertTrue((self._customResponseHeader2) in self._response_headers.decode())
85 self.assertFalse(('UPPERCASE: VaLuE' in self._response_headers.decode()))
86 self.assertTrue(('uppercase: VaLuE' in self._response_headers.decode()))
87 self.assertTrue(('cache-control: max-age=3600' in self._response_headers.decode()))
88 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
89 self.assertEqual(response, receivedResponse)
90 self.checkHasHeader('cache-control', 'max-age=3600')
91
92 def testDOHTransactionID(self):
93 """
94 DOH: Simple query with ID != 0
95 """
96 name = 'simple-with-non-zero-id.doh.tests.powerdns.com.'
97 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
98 query.id = 42
99 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
100 expectedQuery.id = 0
101 response = dns.message.make_response(query)
102 rrset = dns.rrset.from_text(name,
103 3600,
104 dns.rdataclass.IN,
105 dns.rdatatype.A,
106 '127.0.0.1')
107 response.answer.append(rrset)
108
109 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert)
110 self.assertTrue(receivedQuery)
111 self.assertTrue(receivedResponse)
112 receivedQuery.id = expectedQuery.id
113 self.assertEqual(expectedQuery, receivedQuery)
114 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
115 self.assertEqual(response, receivedResponse)
116 # just to be sure the ID _is_ checked
117 self.assertEqual(response.id, receivedResponse.id)
118
119 def testDOHSimplePOST(self):
120 """
121 DOH: Simple POST query
122 """
123 name = 'simple-post.doh.tests.powerdns.com.'
124 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
125 query.id = 0
126 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
127 expectedQuery.id = 0
128 response = dns.message.make_response(query)
129 rrset = dns.rrset.from_text(name,
130 3600,
131 dns.rdataclass.IN,
132 dns.rdatatype.A,
133 '127.0.0.1')
134 response.answer.append(rrset)
135
136 (receivedQuery, receivedResponse) = self.sendDOHPostQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert)
137 self.assertTrue(receivedQuery)
138 self.assertTrue(receivedResponse)
139 receivedQuery.id = expectedQuery.id
140 self.assertEqual(expectedQuery, receivedQuery)
141 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
142 self.assertEqual(response, receivedResponse)
143
144 def testDOHExistingEDNS(self):
145 """
146 DOH: Existing EDNS
147 """
148 name = 'existing-edns.doh.tests.powerdns.com.'
149 query = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=8192)
150 query.id = 0
151 response = dns.message.make_response(query)
152 rrset = dns.rrset.from_text(name,
153 3600,
154 dns.rdataclass.IN,
155 dns.rdatatype.A,
156 '127.0.0.1')
157 response.answer.append(rrset)
158
159 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert)
160 self.assertTrue(receivedQuery)
161 self.assertTrue(receivedResponse)
162 receivedQuery.id = query.id
163 self.assertEqual(query, receivedQuery)
164 self.assertEqual(response, receivedResponse)
165 self.checkQueryEDNSWithoutECS(query, receivedQuery)
166 self.checkResponseEDNSWithoutECS(response, receivedResponse)
167
168 def testDOHExistingECS(self):
169 """
170 DOH: Existing EDNS Client Subnet
171 """
172 name = 'existing-ecs.doh.tests.powerdns.com.'
173 ecso = clientsubnetoption.ClientSubnetOption('1.2.3.4')
174 rewrittenEcso = clientsubnetoption.ClientSubnetOption('127.0.0.1', 24)
175 query = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=512, options=[ecso], want_dnssec=True)
176 query.id = 0
177 response = dns.message.make_response(query)
178 response.use_edns(edns=True, payload=4096, options=[rewrittenEcso])
179 rrset = dns.rrset.from_text(name,
180 3600,
181 dns.rdataclass.IN,
182 dns.rdatatype.A,
183 '127.0.0.1')
184 response.answer.append(rrset)
185
186 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert)
187 self.assertTrue(receivedQuery)
188 self.assertTrue(receivedResponse)
189 receivedQuery.id = query.id
190 self.assertEqual(query, receivedQuery)
191 self.assertEqual(response, receivedResponse)
192 self.checkQueryEDNSWithECS(query, receivedQuery)
193 self.checkResponseEDNSWithECS(response, receivedResponse)
194
195 def testDropped(self):
196 """
197 DOH: Dropped query
198 """
199 name = 'drop.doh.tests.powerdns.com.'
200 query = dns.message.make_query(name, 'A', 'IN')
201 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, caFile=self._caCert, query=query, response=None, useQueue=False)
202 self.assertEqual(receivedResponse, None)
203
204 def testRefused(self):
205 """
206 DOH: Refused
207 """
208 name = 'refused.doh.tests.powerdns.com.'
209 query = dns.message.make_query(name, 'A', 'IN')
210 query.id = 0
211 query.flags &= ~dns.flags.RD
212 expectedResponse = dns.message.make_response(query)
213 expectedResponse.set_rcode(dns.rcode.REFUSED)
214
215 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, caFile=self._caCert, query=query, response=None, useQueue=False)
216 self.assertEqual(receivedResponse, expectedResponse)
217
218 def testSpoof(self):
219 """
220 DOH: Spoofed
221 """
222 name = 'spoof.doh.tests.powerdns.com.'
223 query = dns.message.make_query(name, 'A', 'IN')
224 query.id = 0
225 query.flags &= ~dns.flags.RD
226 expectedResponse = dns.message.make_response(query)
227 rrset = dns.rrset.from_text(name,
228 3600,
229 dns.rdataclass.IN,
230 dns.rdatatype.A,
231 '1.2.3.4')
232 expectedResponse.answer.append(rrset)
233
234 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, caFile=self._caCert, query=query, response=None, useQueue=False)
235 self.assertEqual(receivedResponse, expectedResponse)
236
237 def testDOHInvalid(self):
238 """
239 DOH: Invalid query
240 """
241 name = 'invalid.doh.tests.powerdns.com.'
242 invalidQuery = dns.message.make_query(name, 'A', 'IN', use_edns=False)
243 invalidQuery.id = 0
244 # first an invalid query
245 invalidQuery = invalidQuery.to_wire()
246 invalidQuery = invalidQuery[:-5]
247 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, caFile=self._caCert, query=invalidQuery, response=None, useQueue=False, rawQuery=True)
248 self.assertEqual(receivedResponse, None)
249
250 # and now a valid one
251 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
252 query.id = 0
253 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
254 expectedQuery.id = 0
255 response = dns.message.make_response(query)
256 rrset = dns.rrset.from_text(name,
257 3600,
258 dns.rdataclass.IN,
259 dns.rdatatype.A,
260 '127.0.0.1')
261 response.answer.append(rrset)
262 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert)
263 self.assertTrue(receivedQuery)
264 self.assertTrue(receivedResponse)
265 receivedQuery.id = expectedQuery.id
266 self.assertEqual(expectedQuery, receivedQuery)
267 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
268 self.assertEqual(response, receivedResponse)
269
270 def testDOHWithoutQuery(self):
271 """
272 DOH: Empty GET query
273 """
274 name = 'empty-get.doh.tests.powerdns.com.'
275 url = self._dohBaseURL
276 conn = self.openDOHConnection(self._dohServerPort, self._caCert, timeout=2.0)
277 conn.setopt(pycurl.URL, url)
278 conn.setopt(pycurl.RESOLVE, ["%s:%d:127.0.0.1" % (self._serverName, self._dohServerPort)])
279 conn.setopt(pycurl.SSL_VERIFYPEER, 1)
280 conn.setopt(pycurl.SSL_VERIFYHOST, 2)
281 conn.setopt(pycurl.CAINFO, self._caCert)
282 data = conn.perform_rb()
283 rcode = conn.getinfo(pycurl.RESPONSE_CODE)
284 self.assertEqual(rcode, 400)
285
286 def testDOHEmptyPOST(self):
287 """
288 DOH: Empty POST query
289 """
290 name = 'empty-post.doh.tests.powerdns.com.'
291
292 (_, receivedResponse) = self.sendDOHPostQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query="", rawQuery=True, response=None, caFile=self._caCert)
293 self.assertEqual(receivedResponse, None)
294
295 # and now a valid one
296 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
297 query.id = 0
298 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
299 expectedQuery.id = 0
300 response = dns.message.make_response(query)
301 rrset = dns.rrset.from_text(name,
302 3600,
303 dns.rdataclass.IN,
304 dns.rdatatype.A,
305 '127.0.0.1')
306 response.answer.append(rrset)
307 (receivedQuery, receivedResponse) = self.sendDOHPostQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert)
308 self.assertTrue(receivedQuery)
309 self.assertTrue(receivedResponse)
310 receivedQuery.id = expectedQuery.id
311 self.assertEqual(expectedQuery, receivedQuery)
312 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
313 self.assertEqual(response, receivedResponse)
314
315 def testHeaderRule(self):
316 """
317 DOH: HeaderRule
318 """
319 name = 'header-rule.doh.tests.powerdns.com.'
320 query = dns.message.make_query(name, 'A', 'IN')
321 query.id = 0
322 query.flags &= ~dns.flags.RD
323 expectedResponse = dns.message.make_response(query)
324 rrset = dns.rrset.from_text(name,
325 3600,
326 dns.rdataclass.IN,
327 dns.rdatatype.A,
328 '2.3.4.5')
329 expectedResponse.answer.append(rrset)
330
331 # this header should match
332 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, caFile=self._caCert, query=query, response=None, useQueue=False, customHeaders=['x-powerdnS: aaaaa'])
333 self.assertEqual(receivedResponse, expectedResponse)
334
335 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
336 expectedQuery.flags &= ~dns.flags.RD
337 expectedQuery.id = 0
338 response = dns.message.make_response(query)
339 rrset = dns.rrset.from_text(name,
340 3600,
341 dns.rdataclass.IN,
342 dns.rdatatype.A,
343 '127.0.0.1')
344 response.answer.append(rrset)
345
346 # this content of the header should NOT match
347 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert, customHeaders=['x-powerdnS: bbbbb'])
348 self.assertTrue(receivedQuery)
349 self.assertTrue(receivedResponse)
350 receivedQuery.id = expectedQuery.id
351 self.assertEqual(expectedQuery, receivedQuery)
352 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
353 self.assertEqual(response, receivedResponse)
354
355 def testHTTPPath(self):
356 """
357 DOH: HTTPPath
358 """
359 name = 'http-path.doh.tests.powerdns.com.'
360 query = dns.message.make_query(name, 'A', 'IN')
361 query.id = 0
362 query.flags &= ~dns.flags.RD
363 expectedResponse = dns.message.make_response(query)
364 rrset = dns.rrset.from_text(name,
365 3600,
366 dns.rdataclass.IN,
367 dns.rdatatype.A,
368 '3.4.5.6')
369 expectedResponse.answer.append(rrset)
370
371 # this path should match
372 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL + 'PowerDNS', caFile=self._caCert, query=query, response=None, useQueue=False)
373 self.assertEqual(receivedResponse, expectedResponse)
374
375 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
376 expectedQuery.id = 0
377 expectedQuery.flags &= ~dns.flags.RD
378 response = dns.message.make_response(query)
379 rrset = dns.rrset.from_text(name,
380 3600,
381 dns.rdataclass.IN,
382 dns.rdatatype.A,
383 '127.0.0.1')
384 response.answer.append(rrset)
385
386 # this path should NOT match
387 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL + "PowerDNS2", query, response=response, caFile=self._caCert)
388 self.assertTrue(receivedQuery)
389 self.assertTrue(receivedResponse)
390 receivedQuery.id = expectedQuery.id
391 self.assertEqual(expectedQuery, receivedQuery)
392 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
393 self.assertEqual(response, receivedResponse)
394
395 # this path is not in the URLs map and should lead to a 404
396 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL + "PowerDNS/something", query, caFile=self._caCert, useQueue=False, rawResponse=True)
397 self.assertTrue(receivedResponse)
398 self.assertEqual(receivedResponse, b'there is no endpoint configured for this path')
399 self.assertEqual(self._rcode, 404)
400
401 def testHTTPPathRegex(self):
402 """
403 DOH: HTTPPathRegex
404 """
405 name = 'http-path-regex.doh.tests.powerdns.com.'
406 query = dns.message.make_query(name, 'A', 'IN')
407 query.id = 0
408 query.flags &= ~dns.flags.RD
409 expectedResponse = dns.message.make_response(query)
410 rrset = dns.rrset.from_text(name,
411 3600,
412 dns.rdataclass.IN,
413 dns.rdatatype.A,
414 '6.7.8.9')
415 expectedResponse.answer.append(rrset)
416
417 # this path should match
418 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL + 'PowerDNS-999', caFile=self._caCert, query=query, response=None, useQueue=False)
419 self.assertEqual(receivedResponse, expectedResponse)
420
421 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
422 expectedQuery.id = 0
423 expectedQuery.flags &= ~dns.flags.RD
424 response = dns.message.make_response(query)
425 rrset = dns.rrset.from_text(name,
426 3600,
427 dns.rdataclass.IN,
428 dns.rdatatype.A,
429 '127.0.0.1')
430 response.answer.append(rrset)
431
432 # this path should NOT match
433 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL + "PowerDNS2", query, response=response, caFile=self._caCert)
434 self.assertTrue(receivedQuery)
435 self.assertTrue(receivedResponse)
436 receivedQuery.id = expectedQuery.id
437 self.assertEqual(expectedQuery, receivedQuery)
438 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
439 self.assertEqual(response, receivedResponse)
440
441 def testHTTPStatusAction200(self):
442 """
443 DOH: HTTPStatusAction 200 OK
444 """
445 name = 'http-status-action.doh.tests.powerdns.com.'
446 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
447 query.id = 0
448
449 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, caFile=self._caCert, useQueue=False, rawResponse=True)
450 self.assertTrue(receivedResponse)
451 self.assertEqual(receivedResponse, b'Plaintext answer')
452 self.assertEqual(self._rcode, 200)
453 self.assertTrue('content-type: text/plain' in self._response_headers.decode())
454
455 def testHTTPStatusAction307(self):
456 """
457 DOH: HTTPStatusAction 307
458 """
459 name = 'http-status-action-redirect.doh.tests.powerdns.com.'
460 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
461 query.id = 0
462
463 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, caFile=self._caCert, useQueue=False, rawResponse=True)
464 self.assertTrue(receivedResponse)
465 self.assertEqual(self._rcode, 307)
466 self.assertTrue('location: https://doh.powerdns.org' in self._response_headers.decode())
467
468 def testHTTPLuaResponse(self):
469 """
470 DOH: Lua HTTP Response
471 """
472 name = 'http-lua.doh.tests.powerdns.com.'
473 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
474 query.id = 0
475
476 (_, receivedResponse) = self.sendDOHPostQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, caFile=self._caCert, useQueue=False, rawResponse=True)
477 self.assertTrue(receivedResponse)
478 self.assertEqual(receivedResponse, b'It works!')
479 self.assertEqual(self._rcode, 200)
480 self.assertTrue('content-type: text/plain' in self._response_headers.decode())
481
482 def testHTTPEarlyResponse(self):
483 """
484 DOH: HTTP Early Response
485 """
486 response_headers = BytesIO()
487 url = self._dohBaseURL + 'coffee'
488 conn = self.openDOHConnection(self._dohServerPort, caFile=self._caCert, timeout=2.0)
489 conn.setopt(pycurl.URL, url)
490 conn.setopt(pycurl.RESOLVE, ["%s:%d:127.0.0.1" % (self._serverName, self._dohServerPort)])
491 conn.setopt(pycurl.SSL_VERIFYPEER, 1)
492 conn.setopt(pycurl.SSL_VERIFYHOST, 2)
493 conn.setopt(pycurl.CAINFO, self._caCert)
494 conn.setopt(pycurl.HEADERFUNCTION, response_headers.write)
495 data = conn.perform_rb()
496 rcode = conn.getinfo(pycurl.RESPONSE_CODE)
497 headers = response_headers.getvalue().decode()
498
499 self.assertEqual(rcode, 418)
500 self.assertEqual(data, b'C0FFEE')
501 self.assertIn('foo: bar', headers)
502 self.assertNotIn(self._customResponseHeader2, headers)
503
504 response_headers = BytesIO()
505 conn = self.openDOHConnection(self._dohServerPort, caFile=self._caCert, timeout=2.0)
506 conn.setopt(pycurl.URL, url)
507 conn.setopt(pycurl.RESOLVE, ["%s:%d:127.0.0.1" % (self._serverName, self._dohServerPort)])
508 conn.setopt(pycurl.SSL_VERIFYPEER, 1)
509 conn.setopt(pycurl.SSL_VERIFYHOST, 2)
510 conn.setopt(pycurl.CAINFO, self._caCert)
511 conn.setopt(pycurl.HEADERFUNCTION, response_headers.write)
512 conn.setopt(pycurl.POST, True)
513 data = ''
514 conn.setopt(pycurl.POSTFIELDS, data)
515
516 data = conn.perform_rb()
517 rcode = conn.getinfo(pycurl.RESPONSE_CODE)
518 headers = response_headers.getvalue().decode()
519 self.assertEqual(rcode, 418)
520 self.assertEqual(data, b'C0FFEE')
521 self.assertIn('foo: bar', headers)
522 self.assertNotIn(self._customResponseHeader2, headers)
523
524 class TestDOHSubPaths(DNSDistDOHTest):
525
526 _serverKey = 'server.key'
527 _serverCert = 'server.chain'
528 _serverName = 'tls.tests.dnsdist.org'
529 _caCert = 'ca.pem'
530 _dohServerPort = 8443
531 _dohBaseURL = ("https://%s:%d/" % (_serverName, _dohServerPort))
532 _config_template = """
533 newServer{address="127.0.0.1:%s"}
534
535 addAction(AllRule(), SpoofAction("3.4.5.6"))
536
537 addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/PowerDNS" }, {exactPathMatching=false})
538 """
539 _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey']
540
541 def testSubPath(self):
542 """
543 DOH: sub-path
544 """
545 name = 'sub-path.doh.tests.powerdns.com.'
546 query = dns.message.make_query(name, 'A', 'IN')
547 query.id = 0
548 query.flags &= ~dns.flags.RD
549 expectedResponse = dns.message.make_response(query)
550 rrset = dns.rrset.from_text(name,
551 3600,
552 dns.rdataclass.IN,
553 dns.rdatatype.A,
554 '3.4.5.6')
555 expectedResponse.answer.append(rrset)
556
557 # this path should match
558 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL + 'PowerDNS', caFile=self._caCert, query=query, response=None, useQueue=False)
559 self.assertEqual(receivedResponse, expectedResponse)
560
561 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
562 expectedQuery.id = 0
563 expectedQuery.flags &= ~dns.flags.RD
564 response = dns.message.make_response(query)
565 rrset = dns.rrset.from_text(name,
566 3600,
567 dns.rdataclass.IN,
568 dns.rdatatype.A,
569 '127.0.0.1')
570 response.answer.append(rrset)
571
572 # this path is not in the URLs map and should lead to a 404
573 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL + "NotPowerDNS", query, caFile=self._caCert, useQueue=False, rawResponse=True)
574 self.assertTrue(receivedResponse)
575 self.assertEqual(receivedResponse, b'not found')
576 self.assertEqual(self._rcode, 404)
577
578 # this path is below one in the URLs map and exactPathMatching is false, so we should be good
579 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL + 'PowerDNS/something', caFile=self._caCert, query=query, response=None, useQueue=False)
580 self.assertEqual(receivedResponse, expectedResponse)
581
582 class TestDOHAddingECS(DNSDistDOHTest):
583
584 _serverKey = 'server.key'
585 _serverCert = 'server.chain'
586 _serverName = 'tls.tests.dnsdist.org'
587 _caCert = 'ca.pem'
588 _dohServerPort = 8443
589 _dohBaseURL = ("https://%s:%d/" % (_serverName, _dohServerPort))
590 _config_template = """
591 newServer{address="127.0.0.1:%s", useClientSubnet=true}
592 addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" })
593 setECSOverride(true)
594 """
595 _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey']
596
597 def testDOHSimple(self):
598 """
599 DOH with ECS: Simple query
600 """
601 name = 'simple.doh-ecs.tests.powerdns.com.'
602 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
603 query.id = 0
604 rewrittenEcso = clientsubnetoption.ClientSubnetOption('127.0.0.0', 24)
605 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096, options=[rewrittenEcso])
606 response = dns.message.make_response(query)
607 rrset = dns.rrset.from_text(name,
608 3600,
609 dns.rdataclass.IN,
610 dns.rdatatype.A,
611 '127.0.0.1')
612 response.answer.append(rrset)
613
614 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert)
615 self.assertTrue(receivedQuery)
616 self.assertTrue(receivedResponse)
617 expectedQuery.id = receivedQuery.id
618 self.assertEqual(expectedQuery, receivedQuery)
619 self.checkQueryEDNSWithECS(expectedQuery, receivedQuery)
620 self.assertEqual(response, receivedResponse)
621 self.checkResponseNoEDNS(response, receivedResponse)
622
623 def testDOHExistingEDNS(self):
624 """
625 DOH with ECS: Existing EDNS
626 """
627 name = 'existing-edns.doh-ecs.tests.powerdns.com.'
628 query = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=8192)
629 query.id = 0
630 rewrittenEcso = clientsubnetoption.ClientSubnetOption('127.0.0.0', 24)
631 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=8192, options=[rewrittenEcso])
632 response = dns.message.make_response(query)
633 rrset = dns.rrset.from_text(name,
634 3600,
635 dns.rdataclass.IN,
636 dns.rdatatype.A,
637 '127.0.0.1')
638 response.answer.append(rrset)
639
640 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert)
641 self.assertTrue(receivedQuery)
642 self.assertTrue(receivedResponse)
643 receivedQuery.id = expectedQuery.id
644 self.assertEqual(expectedQuery, receivedQuery)
645 self.assertEqual(response, receivedResponse)
646 self.checkQueryEDNSWithECS(expectedQuery, receivedQuery)
647 self.checkResponseEDNSWithoutECS(response, receivedResponse)
648
649 def testDOHExistingECS(self):
650 """
651 DOH with ECS: Existing EDNS Client Subnet
652 """
653 name = 'existing-ecs.doh-ecs.tests.powerdns.com.'
654 ecso = clientsubnetoption.ClientSubnetOption('1.2.3.4')
655 rewrittenEcso = clientsubnetoption.ClientSubnetOption('127.0.0.0', 24)
656 query = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=512, options=[ecso], want_dnssec=True)
657 query.id = 0
658 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=512, options=[rewrittenEcso])
659 response = dns.message.make_response(query)
660 response.use_edns(edns=True, payload=4096, options=[rewrittenEcso])
661 rrset = dns.rrset.from_text(name,
662 3600,
663 dns.rdataclass.IN,
664 dns.rdatatype.A,
665 '127.0.0.1')
666 response.answer.append(rrset)
667
668 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert)
669 self.assertTrue(receivedQuery)
670 self.assertTrue(receivedResponse)
671 receivedQuery.id = expectedQuery.id
672 self.assertEqual(expectedQuery, receivedQuery)
673 self.assertEqual(response, receivedResponse)
674 self.checkQueryEDNSWithECS(expectedQuery, receivedQuery)
675 self.checkResponseEDNSWithECS(response, receivedResponse)
676
677 class TestDOHOverHTTP(DNSDistDOHTest):
678
679 _dohServerPort = 8480
680 _serverName = 'tls.tests.dnsdist.org'
681 _dohBaseURL = ("http://%s:%d/dns-query" % (_serverName, _dohServerPort))
682 _config_template = """
683 newServer{address="127.0.0.1:%s"}
684 addDOHLocal("127.0.0.1:%s")
685 """
686 _config_params = ['_testServerPort', '_dohServerPort']
687 _checkConfigExpectedOutput = b"""No certificate provided for DoH endpoint 127.0.0.1:8480, running in DNS over HTTP mode instead of DNS over HTTPS
688 Configuration 'configs/dnsdist_TestDOHOverHTTP.conf' OK!
689 """
690
691 def testDOHSimple(self):
692 """
693 DOH over HTTP: Simple query
694 """
695 name = 'simple.doh-over-http.tests.powerdns.com.'
696 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
697 query.id = 0
698 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
699 response = dns.message.make_response(query)
700 rrset = dns.rrset.from_text(name,
701 3600,
702 dns.rdataclass.IN,
703 dns.rdatatype.A,
704 '127.0.0.1')
705 response.answer.append(rrset)
706
707 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, useHTTPS=False)
708 self.assertTrue(receivedQuery)
709 self.assertTrue(receivedResponse)
710 expectedQuery.id = receivedQuery.id
711 self.assertEqual(expectedQuery, receivedQuery)
712 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
713 self.assertEqual(response, receivedResponse)
714 self.checkResponseNoEDNS(response, receivedResponse)
715
716 def testDOHSimplePOST(self):
717 """
718 DOH over HTTP: Simple POST query
719 """
720 name = 'simple-post.doh-over-http.tests.powerdns.com.'
721 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
722 query.id = 0
723 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
724 expectedQuery.id = 0
725 response = dns.message.make_response(query)
726 rrset = dns.rrset.from_text(name,
727 3600,
728 dns.rdataclass.IN,
729 dns.rdatatype.A,
730 '127.0.0.1')
731 response.answer.append(rrset)
732
733 (receivedQuery, receivedResponse) = self.sendDOHPostQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, useHTTPS=False)
734 self.assertTrue(receivedQuery)
735 self.assertTrue(receivedResponse)
736 receivedQuery.id = expectedQuery.id
737 self.assertEqual(expectedQuery, receivedQuery)
738 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
739 self.assertEqual(response, receivedResponse)
740 self.checkResponseNoEDNS(response, receivedResponse)
741
742 class TestDOHWithCache(DNSDistDOHTest):
743
744 _serverKey = 'server.key'
745 _serverCert = 'server.chain'
746 _serverName = 'tls.tests.dnsdist.org'
747 _caCert = 'ca.pem'
748 _dohServerPort = 8443
749 _dohBaseURL = ("https://%s:%d/dns-query" % (_serverName, _dohServerPort))
750 _config_template = """
751 newServer{address="127.0.0.1:%s"}
752
753 addDOHLocal("127.0.0.1:%s", "%s", "%s")
754
755 pc = newPacketCache(100, {maxTTL=86400, minTTL=1})
756 getPool(""):setCache(pc)
757 """
758 _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey']
759
760 def testDOHCacheLargeAnswer(self):
761 """
762 DOH with cache: Check that we can cache (and retrieve) large answers
763 """
764 numberOfQueries = 10
765 name = 'large.doh-with-cache.tests.powerdns.com.'
766 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
767 query.id = 0
768 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
769 expectedQuery.id = 0
770 response = dns.message.make_response(query)
771 # we prepare a large answer
772 content = ""
773 for i in range(44):
774 if len(content) > 0:
775 content = content + ', '
776 content = content + (str(i)*50)
777 # pad up to 4096
778 content = content + 'A'*40
779
780 rrset = dns.rrset.from_text(name,
781 3600,
782 dns.rdataclass.IN,
783 dns.rdatatype.TXT,
784 content)
785 response.answer.append(rrset)
786 self.assertEqual(len(response.to_wire()), 4096)
787
788 # first query to fill the cache
789 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert)
790 self.assertTrue(receivedQuery)
791 self.assertTrue(receivedResponse)
792 receivedQuery.id = expectedQuery.id
793 self.assertEqual(expectedQuery, receivedQuery)
794 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
795 self.assertEqual(response, receivedResponse)
796 self.checkHasHeader('cache-control', 'max-age=3600')
797
798 for _ in range(numberOfQueries):
799 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, caFile=self._caCert, useQueue=False)
800 self.assertEqual(receivedResponse, response)
801 self.checkHasHeader('cache-control', 'max-age=' + str(receivedResponse.answer[0].ttl))
802
803 time.sleep(1)
804
805 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, caFile=self._caCert, useQueue=False)
806 self.assertEqual(receivedResponse, response)
807 self.checkHasHeader('cache-control', 'max-age=' + str(receivedResponse.answer[0].ttl))
808
809 def testDOHGetFromUDPCache(self):
810 """
811 DOH with cache: Check that we can retrieve an answer received for a UDP query
812 """
813 name = 'doh-query-insert-udp.doh-with-cache.tests.powerdns.com.'
814 query = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
815 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
816 expectedQuery.id = 0
817 response = dns.message.make_response(query)
818 rrset = dns.rrset.from_text(name,
819 3600,
820 dns.rdataclass.IN,
821 dns.rdatatype.A,
822 '192.0.2.84')
823 response.answer.append(rrset)
824
825 # first query to fill the cache
826 (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
827 self.assertTrue(receivedQuery)
828 self.assertTrue(receivedResponse)
829 receivedQuery.id = expectedQuery.id
830 self.assertEqual(expectedQuery, receivedQuery)
831 self.assertEqual(response, receivedResponse)
832
833 # now we send the exact same query over DoH, we should get a cache hit
834 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, caFile=self._caCert, useQueue=False)
835 self.assertTrue(receivedResponse)
836 self.assertEqual(response, receivedResponse)
837
838 def testDOHInsertIntoUDPCache(self):
839 """
840 DOH with cache: Check that we can retrieve an answer received for a DoH query from UDP
841 """
842 name = 'udp-query-get-doh.doh-with-cache.tests.powerdns.com.'
843 query = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
844 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
845 expectedQuery.id = 0
846 response = dns.message.make_response(query)
847 rrset = dns.rrset.from_text(name,
848 3600,
849 dns.rdataclass.IN,
850 dns.rdatatype.A,
851 '192.0.2.84')
852 response.answer.append(rrset)
853
854 # first query to fill the cache
855 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert)
856 self.assertTrue(receivedQuery)
857 self.assertTrue(receivedResponse)
858 receivedQuery.id = expectedQuery.id
859 self.assertEqual(expectedQuery, receivedQuery)
860 self.assertEqual(response, receivedResponse)
861
862 # now we send the exact same query over DoH, we should get a cache hit
863 (_, receivedResponse) = self.sendUDPQuery(query, response=None, useQueue=False)
864 self.assertTrue(receivedResponse)
865 self.assertEqual(response, receivedResponse)
866
867 def testTruncation(self):
868 """
869 DOH: Truncation over UDP (with cache)
870 """
871 # the query is first forwarded over UDP, leading to a TC=1 answer from the
872 # backend, then over TCP
873 name = 'truncated-udp.doh-with-cache.tests.powerdns.com.'
874 query = dns.message.make_query(name, 'A', 'IN')
875 query.id = 42
876 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
877 expectedQuery.id = 42
878 response = dns.message.make_response(query)
879 rrset = dns.rrset.from_text(name,
880 3600,
881 dns.rdataclass.IN,
882 dns.rdatatype.A,
883 '127.0.0.1')
884 response.answer.append(rrset)
885
886 # first response is a TC=1
887 tcResponse = dns.message.make_response(query)
888 tcResponse.flags |= dns.flags.TC
889 self._toResponderQueue.put(tcResponse, True, 2.0)
890
891 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, caFile=self._caCert, response=response)
892 # first query, received by the responder over UDP
893 self.assertTrue(receivedQuery)
894 receivedQuery.id = expectedQuery.id
895 self.assertEqual(expectedQuery, receivedQuery)
896 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
897
898 # check the response
899 self.assertTrue(receivedResponse)
900 self.assertEqual(response, receivedResponse)
901
902 # check the second query, received by the responder over TCP
903 receivedQuery = self._fromResponderQueue.get(True, 2.0)
904 self.assertTrue(receivedQuery)
905 receivedQuery.id = expectedQuery.id
906 self.assertEqual(expectedQuery, receivedQuery)
907 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
908
909 # now check the cache for a DoH query
910 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, caFile=self._caCert, useQueue=False)
911 self.assertEqual(response, receivedResponse)
912
913 # The TC=1 answer received over UDP will not be cached, because we currently do not cache answers with no records (no TTL)
914 # The TCP one should, however
915 (_, receivedResponse) = self.sendTCPQuery(expectedQuery, response=None, useQueue=False)
916 self.assertEqual(response, receivedResponse)
917
918 def testResponsesReceivedOverUDP(self):
919 """
920 DOH: Check that responses received over UDP are cached (with cache)
921 """
922 name = 'cached-udp.doh-with-cache.tests.powerdns.com.'
923 query = dns.message.make_query(name, 'A', 'IN')
924 query.id = 0
925 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
926 expectedQuery.id = 0
927 response = dns.message.make_response(query)
928 rrset = dns.rrset.from_text(name,
929 3600,
930 dns.rdataclass.IN,
931 dns.rdatatype.A,
932 '127.0.0.1')
933 response.answer.append(rrset)
934
935 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, caFile=self._caCert, response=response)
936 self.assertTrue(receivedQuery)
937 receivedQuery.id = expectedQuery.id
938 self.assertEqual(expectedQuery, receivedQuery)
939 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
940 self.assertTrue(receivedResponse)
941 self.assertEqual(response, receivedResponse)
942
943 # now check the cache for a DoH query
944 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, caFile=self._caCert, useQueue=False)
945 self.assertEqual(response, receivedResponse)
946
947 # Check that the answer is usable for UDP queries as well
948 (_, receivedResponse) = self.sendUDPQuery(expectedQuery, response=None, useQueue=False)
949 self.assertEqual(response, receivedResponse)
950
951 class TestDOHWithoutCacheControl(DNSDistDOHTest):
952
953 _serverKey = 'server.key'
954 _serverCert = 'server.chain'
955 _serverName = 'tls.tests.dnsdist.org'
956 _caCert = 'ca.pem'
957 _dohServerPort = 8443
958 _dohBaseURL = ("https://%s:%d/" % (_serverName, _dohServerPort))
959 _config_template = """
960 newServer{address="127.0.0.1:%s"}
961
962 addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" }, {sendCacheControlHeaders=false})
963 """
964 _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey']
965
966 def testDOHSimple(self):
967 """
968 DOH without cache-control
969 """
970 name = 'simple.doh.tests.powerdns.com.'
971 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
972 query.id = 0
973 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
974 expectedQuery.id = 0
975 response = dns.message.make_response(query)
976 rrset = dns.rrset.from_text(name,
977 3600,
978 dns.rdataclass.IN,
979 dns.rdatatype.A,
980 '127.0.0.1')
981 response.answer.append(rrset)
982
983 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert)
984 self.assertTrue(receivedQuery)
985 self.assertTrue(receivedResponse)
986 receivedQuery.id = expectedQuery.id
987 self.assertEqual(expectedQuery, receivedQuery)
988 self.checkNoHeader('cache-control')
989 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
990 self.assertEqual(response, receivedResponse)
991
992 class TestDOHFFI(DNSDistDOHTest):
993
994 _serverKey = 'server.key'
995 _serverCert = 'server.chain'
996 _serverName = 'tls.tests.dnsdist.org'
997 _caCert = 'ca.pem'
998 _dohServerPort = 8443
999 _customResponseHeader1 = 'access-control-allow-origin: *'
1000 _customResponseHeader2 = 'user-agent: derp'
1001 _dohBaseURL = ("https://%s:%d/" % (_serverName, _dohServerPort))
1002 _config_template = """
1003 newServer{address="127.0.0.1:%s"}
1004
1005 addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" }, {customResponseHeaders={["access-control-allow-origin"]="*",["user-agent"]="derp",["UPPERCASE"]="VaLuE"}, keepIncomingHeaders=true})
1006
1007 local ffi = require("ffi")
1008
1009 function dohHandler(dq)
1010 local scheme = ffi.string(ffi.C.dnsdist_ffi_dnsquestion_get_http_scheme(dq))
1011 local host = ffi.string(ffi.C.dnsdist_ffi_dnsquestion_get_http_host(dq))
1012 local path = ffi.string(ffi.C.dnsdist_ffi_dnsquestion_get_http_path(dq))
1013 local query_string = ffi.string(ffi.C.dnsdist_ffi_dnsquestion_get_http_query_string(dq))
1014 if scheme == 'https' and host == '%s:%d' and path == '/' and query_string == '' then
1015 local foundct = false
1016 local headers_ptr = ffi.new("const dnsdist_ffi_http_header_t *[1]")
1017 local headers_ptr_param = ffi.cast("const dnsdist_ffi_http_header_t **", headers_ptr)
1018
1019 local headers_count = tonumber(ffi.C.dnsdist_ffi_dnsquestion_get_http_headers(dq, headers_ptr_param))
1020 if headers_count > 0 then
1021 for idx = 0, headers_count-1 do
1022 if ffi.string(headers_ptr[0][idx].name) == 'content-type' and ffi.string(headers_ptr[0][idx].value) == 'application/dns-message' then
1023 foundct = true
1024 break
1025 end
1026 end
1027 end
1028 if foundct then
1029 local response = 'It works!'
1030 ffi.C.dnsdist_ffi_dnsquestion_set_http_response(dq, 200, response, #response, 'text/plain')
1031 return DNSAction.HeaderModify
1032 end
1033 end
1034 return DNSAction.None
1035 end
1036 addAction("http-lua-ffi.doh.tests.powerdns.com.", LuaFFIAction(dohHandler))
1037 """
1038 _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey', '_serverName', '_dohServerPort']
1039
1040 def testHTTPLuaFFIResponse(self):
1041 """
1042 DOH: Lua FFI HTTP Response
1043 """
1044 name = 'http-lua-ffi.doh.tests.powerdns.com.'
1045 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
1046 query.id = 0
1047
1048 (_, receivedResponse) = self.sendDOHPostQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, caFile=self._caCert, useQueue=False, rawResponse=True)
1049 self.assertTrue(receivedResponse)
1050 self.assertEqual(receivedResponse, b'It works!')
1051 self.assertEqual(self._rcode, 200)
1052 self.assertTrue('content-type: text/plain' in self._response_headers.decode())
1053
1054 class TestDOHForwardedFor(DNSDistDOHTest):
1055
1056 _serverKey = 'server.key'
1057 _serverCert = 'server.chain'
1058 _serverName = 'tls.tests.dnsdist.org'
1059 _caCert = 'ca.pem'
1060 _dohServerPort = 8443
1061 _dohBaseURL = ("https://%s:%d/" % (_serverName, _dohServerPort))
1062 _config_template = """
1063 newServer{address="127.0.0.1:%s"}
1064
1065 setACL('192.0.2.1/32')
1066 addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" }, {trustForwardedForHeader=true})
1067 """
1068 _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey']
1069
1070 def testDOHAllowedForwarded(self):
1071 """
1072 DOH with X-Forwarded-For allowed
1073 """
1074 name = 'allowed.forwarded.doh.tests.powerdns.com.'
1075 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
1076 query.id = 0
1077 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
1078 expectedQuery.id = 0
1079 response = dns.message.make_response(query)
1080 rrset = dns.rrset.from_text(name,
1081 3600,
1082 dns.rdataclass.IN,
1083 dns.rdatatype.A,
1084 '127.0.0.1')
1085 response.answer.append(rrset)
1086
1087 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert, customHeaders=['x-forwarded-for: 127.0.0.1:42, 127.0.0.1, 192.0.2.1:4200'])
1088 self.assertTrue(receivedQuery)
1089 self.assertTrue(receivedResponse)
1090 receivedQuery.id = expectedQuery.id
1091 self.assertEqual(expectedQuery, receivedQuery)
1092 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
1093 self.assertEqual(response, receivedResponse)
1094
1095 def testDOHDeniedForwarded(self):
1096 """
1097 DOH with X-Forwarded-For not allowed
1098 """
1099 name = 'not-allowed.forwarded.doh.tests.powerdns.com.'
1100 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
1101 query.id = 0
1102 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
1103 expectedQuery.id = 0
1104 response = dns.message.make_response(query)
1105 rrset = dns.rrset.from_text(name,
1106 3600,
1107 dns.rdataclass.IN,
1108 dns.rdatatype.A,
1109 '127.0.0.1')
1110 response.answer.append(rrset)
1111
1112 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert, useQueue=False, rawResponse=True, customHeaders=['x-forwarded-for: 127.0.0.1:42, 127.0.0.1'])
1113
1114 self.assertEqual(self._rcode, 403)
1115 self.assertEqual(receivedResponse, b'dns query not allowed because of ACL')
1116
1117 class TestDOHForwardedForNoTrusted(DNSDistDOHTest):
1118
1119 _serverKey = 'server.key'
1120 _serverCert = 'server.chain'
1121 _serverName = 'tls.tests.dnsdist.org'
1122 _caCert = 'ca.pem'
1123 _dohServerPort = 8443
1124 _dohBaseURL = ("https://%s:%d/" % (_serverName, _dohServerPort))
1125 _config_template = """
1126 newServer{address="127.0.0.1:%s"}
1127
1128 setACL('192.0.2.1/32')
1129 addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" })
1130 """
1131 _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey']
1132
1133 def testDOHForwardedUntrusted(self):
1134 """
1135 DOH with X-Forwarded-For not trusted
1136 """
1137 name = 'not-trusted.forwarded.doh.tests.powerdns.com.'
1138 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
1139 query.id = 0
1140 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
1141 expectedQuery.id = 0
1142 response = dns.message.make_response(query)
1143 rrset = dns.rrset.from_text(name,
1144 3600,
1145 dns.rdataclass.IN,
1146 dns.rdatatype.A,
1147 '127.0.0.1')
1148 response.answer.append(rrset)
1149
1150 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert, useQueue=False, rawResponse=True, customHeaders=['x-forwarded-for: 192.0.2.1:4200'])
1151
1152 self.assertEqual(self._rcode, 403)
1153 self.assertEqual(receivedResponse, b'dns query not allowed because of ACL')
1154
1155 class TestDOHFrontendLimits(DNSDistDOHTest):
1156
1157 # this test suite uses a different responder port
1158 # because it uses a different health check configuration
1159 _testServerPort = 5395
1160 _answerUnexpected = True
1161
1162 _serverKey = 'server.key'
1163 _serverCert = 'server.chain'
1164 _serverName = 'tls.tests.dnsdist.org'
1165 _caCert = 'ca.pem'
1166 _dohServerPort = 8443
1167 _dohBaseURL = ("https://%s:%d/" % (_serverName, _dohServerPort))
1168
1169 _skipListeningOnCL = True
1170 _maxTCPConnsPerDOHFrontend = 5
1171 _config_template = """
1172 newServer{address="127.0.0.1:%s"}
1173 addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" }, { maxConcurrentTCPConnections=%d })
1174 """
1175 _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey', '_maxTCPConnsPerDOHFrontend']
1176
1177 def testTCPConnsPerDOHFrontend(self):
1178 """
1179 DoH Frontend Limits: Maximum number of conns per DoH frontend
1180 """
1181 name = 'maxconnsperfrontend.doh.tests.powerdns.com.'
1182 query = b"GET / HTTP/1.0\r\n\r\n"
1183 conns = []
1184
1185 for idx in range(self._maxTCPConnsPerDOHFrontend + 1):
1186 try:
1187 conns.append(self.openTLSConnection(self._dohServerPort, self._serverName, self._caCert))
1188 except:
1189 conns.append(None)
1190
1191 count = 0
1192 failed = 0
1193 for conn in conns:
1194 if not conn:
1195 failed = failed + 1
1196 continue
1197
1198 try:
1199 conn.send(query)
1200 response = conn.recv(65535)
1201 if response:
1202 count = count + 1
1203 else:
1204 failed = failed + 1
1205 except:
1206 failed = failed + 1
1207
1208 for conn in conns:
1209 if conn:
1210 conn.close()
1211
1212 # wait a bit to be sure that dnsdist closed the connections
1213 # and decremented the counters on its side, otherwise subsequent
1214 # connections will be dropped
1215 time.sleep(1)
1216
1217 self.assertEqual(count, self._maxTCPConnsPerDOHFrontend)
1218 self.assertEqual(failed, 1)
1219
1220 class TestProtocols(DNSDistDOHTest):
1221 _serverKey = 'server.key'
1222 _serverCert = 'server.chain'
1223 _serverName = 'tls.tests.dnsdist.org'
1224 _caCert = 'ca.pem'
1225 _dohServerPort = 8443
1226 _customResponseHeader1 = 'access-control-allow-origin: *'
1227 _customResponseHeader2 = 'user-agent: derp'
1228 _dohBaseURL = ("https://%s:%d/" % (_serverName, _dohServerPort))
1229 _config_template = """
1230 function checkDOH(dq)
1231 if dq:getProtocol() ~= "DNS over HTTPS" then
1232 return DNSAction.Spoof, '1.2.3.4'
1233 end
1234 return DNSAction.None
1235 end
1236
1237 addAction("protocols.doh.tests.powerdns.com.", LuaAction(checkDOH))
1238 newServer{address="127.0.0.1:%s"}
1239 addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" })
1240 """
1241 _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey']
1242
1243 def testProtocolDOH(self):
1244 """
1245 DoH: Test DNSQuestion.Protocol
1246 """
1247 name = 'protocols.doh.tests.powerdns.com.'
1248 query = dns.message.make_query(name, 'A', 'IN')
1249 response = dns.message.make_response(query)
1250 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
1251 expectedQuery.id = 0
1252
1253 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert)
1254 self.assertTrue(receivedQuery)
1255 self.assertTrue(receivedResponse)
1256 receivedQuery.id = expectedQuery.id
1257 self.assertEqual(expectedQuery, receivedQuery)
1258 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
1259 self.assertEqual(response, receivedResponse)
1260
1261 class TestDOHWithPCKS12Cert(DNSDistDOHTest):
1262 _serverCert = 'server.p12'
1263 _pkcs12Password = 'passw0rd'
1264 _serverName = 'tls.tests.dnsdist.org'
1265 _caCert = 'ca.pem'
1266 _dohServerPort = 8443
1267 _dohBaseURL = ("https://%s:%d/" % (_serverName, _dohServerPort))
1268 _config_template = """
1269 newServer{address="127.0.0.1:%s"}
1270 cert=newTLSCertificate("%s", {password="%s"})
1271 addDOHLocal("127.0.0.1:%s", cert, "", { "/" })
1272 """
1273 _config_params = ['_testServerPort', '_serverCert', '_pkcs12Password', '_dohServerPort']
1274
1275 def testProtocolDOH(self):
1276 """
1277 DoH: Test Simple DOH Query with a password protected PCKS12 file configured
1278 """
1279 name = 'simple.doh.tests.powerdns.com.'
1280 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
1281 query.id = 0
1282 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
1283 expectedQuery.id = 0
1284 response = dns.message.make_response(query)
1285 rrset = dns.rrset.from_text(name,
1286 3600,
1287 dns.rdataclass.IN,
1288 dns.rdatatype.A,
1289 '127.0.0.1')
1290 response.answer.append(rrset)
1291
1292 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert)
1293 self.assertTrue(receivedQuery)
1294 self.assertTrue(receivedResponse)
1295 receivedQuery.id = expectedQuery.id
1296 self.assertEqual(expectedQuery, receivedQuery)
1297
1298 class TestDOHForwardedToTCPOnly(DNSDistDOHTest):
1299 _serverKey = 'server.key'
1300 _serverCert = 'server.chain'
1301 _serverName = 'tls.tests.dnsdist.org'
1302 _caCert = 'ca.pem'
1303 _dohServerPort = 8443
1304 _dohBaseURL = ("https://%s:%d/" % (_serverName, _dohServerPort))
1305 _config_template = """
1306 newServer{address="127.0.0.1:%s", tcpOnly=true}
1307 addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" })
1308 """
1309 _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey']
1310
1311 def testDOHTCPOnly(self):
1312 """
1313 DoH: Test a DoH query forwarded to a TCP-only server
1314 """
1315 name = 'tcponly.doh.tests.powerdns.com.'
1316 query = dns.message.make_query(name, 'A', 'IN')
1317 query.id = 42
1318 response = dns.message.make_response(query)
1319 rrset = dns.rrset.from_text(name,
1320 3600,
1321 dns.rdataclass.IN,
1322 dns.rdatatype.A,
1323 '127.0.0.1')
1324 response.answer.append(rrset)
1325
1326 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert)
1327 self.assertTrue(receivedQuery)
1328 self.assertTrue(receivedResponse)
1329 receivedQuery.id = query.id
1330 self.assertEqual(receivedQuery, query)
1331 self.assertEqual(receivedResponse, response)
1332
1333 class TestDOHLimits(DNSDistDOHTest):
1334 _serverName = 'tls.tests.dnsdist.org'
1335 _caCert = 'ca.pem'
1336 _dohServerPort = 8443
1337 _dohBaseURL = ("https://%s:%d/" % (_serverName, _dohServerPort))
1338 _serverKey = 'server.key'
1339 _serverCert = 'server.chain'
1340 _maxTCPConnsPerClient = 3
1341 _config_template = """
1342 newServer{address="127.0.0.1:%s"}
1343 addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" })
1344 setMaxTCPConnectionsPerClient(%s)
1345 """
1346 _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey', '_maxTCPConnsPerClient']
1347
1348 def testConnsPerClient(self):
1349 """
1350 DoH Limits: Maximum number of conns per client
1351 """
1352 name = 'maxconnsperclient.doh.tests.powerdns.com.'
1353 query = dns.message.make_query(name, 'A', 'IN')
1354 url = self.getDOHGetURL(self._dohBaseURL, query)
1355 conns = []
1356
1357 for idx in range(self._maxTCPConnsPerClient + 1):
1358 conn = self.openDOHConnection(self._dohServerPort, self._caCert, timeout=2.0)
1359 conn.setopt(pycurl.URL, url)
1360 conn.setopt(pycurl.RESOLVE, ["%s:%d:127.0.0.1" % (self._serverName, self._dohServerPort)])
1361 conn.setopt(pycurl.SSL_VERIFYPEER, 1)
1362 conn.setopt(pycurl.SSL_VERIFYHOST, 2)
1363 conn.setopt(pycurl.CAINFO, self._caCert)
1364 conns.append(conn)
1365
1366 count = 0
1367 failed = 0
1368 for conn in conns:
1369 try:
1370 data = conn.perform_rb()
1371 rcode = conn.getinfo(pycurl.RESPONSE_CODE)
1372 count = count + 1
1373 except:
1374 failed = failed + 1
1375
1376 for conn in conns:
1377 conn.close()
1378
1379 # wait a bit to be sure that dnsdist closed the connections
1380 # and decremented the counters on its side, otherwise subsequent
1381 # connections will be dropped
1382 time.sleep(1)
1383
1384 self.assertEqual(count, self._maxTCPConnsPerClient)
1385 self.assertEqual(failed, 1)