8 import clientsubnetoption
9 from dnsdisttests
import DNSDistTest
12 from io
import BytesIO
14 @unittest.skipIf('SKIP_DOH_TESTS' in os
.environ
, 'DNS over HTTPS tests are disabled')
15 class DNSDistDOHTest(DNSDistTest
):
18 def getDOHGetURL(cls
, baseurl
, query
, rawQuery
=False):
22 wire
= query
.to_wire()
23 param
= base64
.urlsafe_b64encode(wire
).decode('UTF8').rstrip('=')
24 return baseurl
+ "?dns=" + param
27 def openDOHConnection(cls
, port
, caFile
, timeout
=2.0):
29 conn
.setopt(pycurl
.HTTP_VERSION
, pycurl
.CURL_HTTP_VERSION_2
)
31 conn
.setopt(pycurl
.HTTPHEADER
, ["Content-type: application/dns-message",
32 "Accept: application/dns-message"])
36 def sendDOHQuery(cls
, port
, servername
, baseurl
, query
, response
=None, timeout
=2.0, caFile
=None, useQueue
=True, rawQuery
=False, rawResponse
=False, customHeaders
=[], useHTTPS
=True):
37 url
= cls
.getDOHGetURL(baseurl
, query
, rawQuery
)
38 conn
= cls
.openDOHConnection(port
, caFile
=caFile
, timeout
=timeout
)
39 response_headers
= BytesIO()
40 #conn.setopt(pycurl.VERBOSE, True)
41 conn
.setopt(pycurl
.URL
, url
)
42 conn
.setopt(pycurl
.RESOLVE
, ["%s:%d:127.0.0.1" % (servername
, port
)])
44 conn
.setopt(pycurl
.SSL_VERIFYPEER
, 1)
45 conn
.setopt(pycurl
.SSL_VERIFYHOST
, 2)
47 conn
.setopt(pycurl
.CAINFO
, caFile
)
49 conn
.setopt(pycurl
.HTTPHEADER
, customHeaders
)
50 conn
.setopt(pycurl
.HEADERFUNCTION
, response_headers
.write
)
53 cls
._toResponderQueue
.put(response
, True, timeout
)
57 cls
._response
_headers
= ''
58 data
= conn
.perform_rb()
59 cls
._rcode
= conn
.getinfo(pycurl
.RESPONSE_CODE
)
60 if cls
._rcode
== 200 and not rawResponse
:
61 message
= dns
.message
.from_wire(data
)
65 if useQueue
and not cls
._fromResponderQueue
.empty():
66 receivedQuery
= cls
._fromResponderQueue
.get(True, timeout
)
68 cls
._response
_headers
= response_headers
.getvalue()
69 return (receivedQuery
, message
)
72 def sendDOHPostQuery(cls
, port
, servername
, baseurl
, query
, response
=None, timeout
=2.0, caFile
=None, useQueue
=True, rawQuery
=False, rawResponse
=False, customHeaders
=[], useHTTPS
=True):
74 conn
= cls
.openDOHConnection(port
, caFile
=caFile
, timeout
=timeout
)
75 response_headers
= BytesIO()
76 #conn.setopt(pycurl.VERBOSE, True)
77 conn
.setopt(pycurl
.URL
, url
)
78 conn
.setopt(pycurl
.RESOLVE
, ["%s:%d:127.0.0.1" % (servername
, port
)])
80 conn
.setopt(pycurl
.SSL_VERIFYPEER
, 1)
81 conn
.setopt(pycurl
.SSL_VERIFYHOST
, 2)
83 conn
.setopt(pycurl
.CAINFO
, caFile
)
85 conn
.setopt(pycurl
.HTTPHEADER
, customHeaders
)
86 conn
.setopt(pycurl
.HEADERFUNCTION
, response_headers
.write
)
87 conn
.setopt(pycurl
.POST
, True)
92 conn
.setopt(pycurl
.POSTFIELDS
, data
)
95 cls
._toResponderQueue
.put(response
, True, timeout
)
99 cls
._response
_headers
= ''
100 data
= conn
.perform_rb()
101 cls
._rcode
= conn
.getinfo(pycurl
.RESPONSE_CODE
)
102 if cls
._rcode
== 200 and not rawResponse
:
103 message
= dns
.message
.from_wire(data
)
107 if useQueue
and not cls
._fromResponderQueue
.empty():
108 receivedQuery
= cls
._fromResponderQueue
.get(True, timeout
)
110 cls
._response
_headers
= response_headers
.getvalue()
111 return (receivedQuery
, message
)
113 def getHeaderValue(self
, name
):
114 for header
in self
._response
_headers
.decode().splitlines(False):
115 values
= header
.split(':')
117 if key
.lower() == name
.lower():
118 return values
[1].strip()
121 def checkHasHeader(self
, name
, value
):
122 got
= self
.getHeaderValue(name
)
123 self
.assertEquals(got
, value
)
125 def checkNoHeader(self
, name
):
126 self
.checkHasHeader(name
, None)
131 # for some reason, @unittest.skipIf() is not applied to derived classes with some versions of Python
132 if 'SKIP_DOH_TESTS' in os
.environ
:
133 raise unittest
.SkipTest('DNS over HTTPS tests are disabled')
135 cls
.startResponders()
139 print("Launching tests..")
141 class TestDOH(DNSDistDOHTest
):
143 _serverKey
= 'server.key'
144 _serverCert
= 'server.chain'
145 _serverName
= 'tls.tests.dnsdist.org'
147 _dohServerPort
= 8443
148 _customResponseHeader1
= 'access-control-allow-origin: *'
149 _customResponseHeader2
= 'user-agent: derp'
150 _dohBaseURL
= ("https://%s:%d/" % (_serverName
, _dohServerPort
))
151 _config_template
= """
152 newServer{address="127.0.0.1:%s"}
154 addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" }, {customResponseHeaders={["access-control-allow-origin"]="*",["user-agent"]="derp",["UPPERCASE"]="VaLuE"}})
155 dohFE = getDOHFrontend(0)
156 dohFE:setResponsesMap({newDOHResponseMapEntry('^/coffee$', 418, 'C0FFEE', {['FoO']='bar'})})
158 addAction("drop.doh.tests.powerdns.com.", DropAction())
159 addAction("refused.doh.tests.powerdns.com.", RCodeAction(DNSRCode.REFUSED))
160 addAction("spoof.doh.tests.powerdns.com.", SpoofAction("1.2.3.4"))
161 addAction(HTTPHeaderRule("X-PowerDNS", "^[a]{5}$"), SpoofAction("2.3.4.5"))
162 addAction(HTTPPathRule("/PowerDNS"), SpoofAction("3.4.5.6"))
163 addAction(HTTPPathRegexRule("^/PowerDNS-[0-9]"), SpoofAction("6.7.8.9"))
164 addAction("http-status-action.doh.tests.powerdns.com.", HTTPStatusAction(200, "Plaintext answer", "text/plain"))
165 addAction("http-status-action-redirect.doh.tests.powerdns.com.", HTTPStatusAction(307, "https://doh.powerdns.org"))
167 function dohHandler(dq)
168 if dq:getHTTPScheme() == 'https' and dq:getHTTPHost() == '%s:%d' and dq:getHTTPPath() == '/' and dq:getHTTPQueryString() == '' then
169 local foundct = false
170 for key,value in pairs(dq:getHTTPHeaders()) do
171 if key == 'content-type' and value == 'application/dns-message' then
177 dq:setHTTPResponse(200, 'It works!', 'text/plain')
179 return DNSAction.HeaderModify
182 return DNSAction.None
184 addAction("http-lua.doh.tests.powerdns.com.", LuaAction(dohHandler))
186 _config_params
= ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey', '_serverName', '_dohServerPort']
188 def testDOHSimple(self
):
192 name
= 'simple.doh.tests.powerdns.com.'
193 query
= dns
.message
.make_query(name
, 'A', 'IN', use_edns
=False)
195 expectedQuery
= dns
.message
.make_query(name
, 'A', 'IN', use_edns
=True, payload
=4096)
197 response
= dns
.message
.make_response(query
)
198 rrset
= dns
.rrset
.from_text(name
,
203 response
.answer
.append(rrset
)
205 (receivedQuery
, receivedResponse
) = self
.sendDOHQuery(self
._dohServerPort
, self
._serverName
, self
._dohBaseURL
, query
, response
=response
, caFile
=self
._caCert
)
206 self
.assertTrue(receivedQuery
)
207 self
.assertTrue(receivedResponse
)
208 receivedQuery
.id = expectedQuery
.id
209 self
.assertEquals(expectedQuery
, receivedQuery
)
210 self
.assertTrue((self
._customResponseHeader
1) in self
._response
_headers
.decode())
211 self
.assertTrue((self
._customResponseHeader
2) in self
._response
_headers
.decode())
212 self
.assertFalse(('UPPERCASE: VaLuE' in self
._response
_headers
.decode()))
213 self
.assertTrue(('uppercase: VaLuE' in self
._response
_headers
.decode()))
214 self
.assertTrue(('cache-control: max-age=3600' in self
._response
_headers
.decode()))
215 self
.checkQueryEDNSWithoutECS(expectedQuery
, receivedQuery
)
216 self
.assertEquals(response
, receivedResponse
)
217 self
.checkHasHeader('cache-control', 'max-age=3600')
219 def testDOHSimplePOST(self
):
221 DOH: Simple POST query
223 name
= 'simple-post.doh.tests.powerdns.com.'
224 query
= dns
.message
.make_query(name
, 'A', 'IN', use_edns
=False)
226 expectedQuery
= dns
.message
.make_query(name
, 'A', 'IN', use_edns
=True, payload
=4096)
228 response
= dns
.message
.make_response(query
)
229 rrset
= dns
.rrset
.from_text(name
,
234 response
.answer
.append(rrset
)
236 (receivedQuery
, receivedResponse
) = self
.sendDOHPostQuery(self
._dohServerPort
, self
._serverName
, self
._dohBaseURL
, query
, response
=response
, caFile
=self
._caCert
)
237 self
.assertTrue(receivedQuery
)
238 self
.assertTrue(receivedResponse
)
239 receivedQuery
.id = expectedQuery
.id
240 self
.assertEquals(expectedQuery
, receivedQuery
)
241 self
.checkQueryEDNSWithoutECS(expectedQuery
, receivedQuery
)
242 self
.assertEquals(response
, receivedResponse
)
244 def testDOHExistingEDNS(self
):
248 name
= 'existing-edns.doh.tests.powerdns.com.'
249 query
= dns
.message
.make_query(name
, 'A', 'IN', use_edns
=True, payload
=8192)
251 response
= dns
.message
.make_response(query
)
252 rrset
= dns
.rrset
.from_text(name
,
257 response
.answer
.append(rrset
)
259 (receivedQuery
, receivedResponse
) = self
.sendDOHQuery(self
._dohServerPort
, self
._serverName
, self
._dohBaseURL
, query
, response
=response
, caFile
=self
._caCert
)
260 self
.assertTrue(receivedQuery
)
261 self
.assertTrue(receivedResponse
)
262 receivedQuery
.id = query
.id
263 self
.assertEquals(query
, receivedQuery
)
264 self
.assertEquals(response
, receivedResponse
)
265 self
.checkQueryEDNSWithoutECS(query
, receivedQuery
)
266 self
.checkResponseEDNSWithoutECS(response
, receivedResponse
)
268 def testDOHExistingECS(self
):
270 DOH: Existing EDNS Client Subnet
272 name
= 'existing-ecs.doh.tests.powerdns.com.'
273 ecso
= clientsubnetoption
.ClientSubnetOption('1.2.3.4')
274 rewrittenEcso
= clientsubnetoption
.ClientSubnetOption('127.0.0.1', 24)
275 query
= dns
.message
.make_query(name
, 'A', 'IN', use_edns
=True, payload
=512, options
=[ecso
], want_dnssec
=True)
277 response
= dns
.message
.make_response(query
)
278 response
.use_edns(edns
=True, payload
=4096, options
=[rewrittenEcso
])
279 rrset
= dns
.rrset
.from_text(name
,
284 response
.answer
.append(rrset
)
286 (receivedQuery
, receivedResponse
) = self
.sendDOHQuery(self
._dohServerPort
, self
._serverName
, self
._dohBaseURL
, query
, response
=response
, caFile
=self
._caCert
)
287 self
.assertTrue(receivedQuery
)
288 self
.assertTrue(receivedResponse
)
289 receivedQuery
.id = query
.id
290 self
.assertEquals(query
, receivedQuery
)
291 self
.assertEquals(response
, receivedResponse
)
292 self
.checkQueryEDNSWithECS(query
, receivedQuery
)
293 self
.checkResponseEDNSWithECS(response
, receivedResponse
)
295 def testDropped(self
):
299 name
= 'drop.doh.tests.powerdns.com.'
300 query
= dns
.message
.make_query(name
, 'A', 'IN')
301 (_
, receivedResponse
) = self
.sendDOHQuery(self
._dohServerPort
, self
._serverName
, self
._dohBaseURL
, caFile
=self
._caCert
, query
=query
, response
=None, useQueue
=False)
302 self
.assertEquals(receivedResponse
, None)
304 def testRefused(self
):
308 name
= 'refused.doh.tests.powerdns.com.'
309 query
= dns
.message
.make_query(name
, 'A', 'IN')
311 query
.flags
&= ~dns
.flags
.RD
312 expectedResponse
= dns
.message
.make_response(query
)
313 expectedResponse
.set_rcode(dns
.rcode
.REFUSED
)
315 (_
, receivedResponse
) = self
.sendDOHQuery(self
._dohServerPort
, self
._serverName
, self
._dohBaseURL
, caFile
=self
._caCert
, query
=query
, response
=None, useQueue
=False)
316 self
.assertEquals(receivedResponse
, expectedResponse
)
322 name
= 'spoof.doh.tests.powerdns.com.'
323 query
= dns
.message
.make_query(name
, 'A', 'IN')
325 query
.flags
&= ~dns
.flags
.RD
326 expectedResponse
= dns
.message
.make_response(query
)
327 rrset
= dns
.rrset
.from_text(name
,
332 expectedResponse
.answer
.append(rrset
)
334 (_
, receivedResponse
) = self
.sendDOHQuery(self
._dohServerPort
, self
._serverName
, self
._dohBaseURL
, caFile
=self
._caCert
, query
=query
, response
=None, useQueue
=False)
335 self
.assertEquals(receivedResponse
, expectedResponse
)
337 def testDOHInvalid(self
):
341 name
= 'invalid.doh.tests.powerdns.com.'
342 invalidQuery
= dns
.message
.make_query(name
, 'A', 'IN', use_edns
=False)
344 # first an invalid query
345 invalidQuery
= invalidQuery
.to_wire()
346 invalidQuery
= invalidQuery
[:-5]
347 (_
, receivedResponse
) = self
.sendDOHQuery(self
._dohServerPort
, self
._serverName
, self
._dohBaseURL
, caFile
=self
._caCert
, query
=invalidQuery
, response
=None, useQueue
=False, rawQuery
=True)
348 self
.assertEquals(receivedResponse
, None)
350 # and now a valid one
351 query
= dns
.message
.make_query(name
, 'A', 'IN', use_edns
=False)
353 expectedQuery
= dns
.message
.make_query(name
, 'A', 'IN', use_edns
=True, payload
=4096)
355 response
= dns
.message
.make_response(query
)
356 rrset
= dns
.rrset
.from_text(name
,
361 response
.answer
.append(rrset
)
362 (receivedQuery
, receivedResponse
) = self
.sendDOHQuery(self
._dohServerPort
, self
._serverName
, self
._dohBaseURL
, query
, response
=response
, caFile
=self
._caCert
)
363 self
.assertTrue(receivedQuery
)
364 self
.assertTrue(receivedResponse
)
365 receivedQuery
.id = expectedQuery
.id
366 self
.assertEquals(expectedQuery
, receivedQuery
)
367 self
.checkQueryEDNSWithoutECS(expectedQuery
, receivedQuery
)
368 self
.assertEquals(response
, receivedResponse
)
370 def testDOHWithoutQuery(self
):
374 name
= 'empty-get.doh.tests.powerdns.com.'
375 url
= self
._dohBaseURL
376 conn
= self
.openDOHConnection(self
._dohServerPort
, self
._caCert
, timeout
=2.0)
377 conn
.setopt(pycurl
.URL
, url
)
378 conn
.setopt(pycurl
.RESOLVE
, ["%s:%d:127.0.0.1" % (self
._serverName
, self
._dohServerPort
)])
379 conn
.setopt(pycurl
.SSL_VERIFYPEER
, 1)
380 conn
.setopt(pycurl
.SSL_VERIFYHOST
, 2)
381 conn
.setopt(pycurl
.CAINFO
, self
._caCert
)
382 data
= conn
.perform_rb()
383 rcode
= conn
.getinfo(pycurl
.RESPONSE_CODE
)
384 self
.assertEquals(rcode
, 400)
386 def testDOHEmptyPOST(self
):
388 DOH: Empty POST query
390 name
= 'empty-post.doh.tests.powerdns.com.'
392 (_
, receivedResponse
) = self
.sendDOHPostQuery(self
._dohServerPort
, self
._serverName
, self
._dohBaseURL
, query
="", rawQuery
=True, response
=None, caFile
=self
._caCert
)
393 self
.assertEquals(receivedResponse
, None)
395 # and now a valid one
396 query
= dns
.message
.make_query(name
, 'A', 'IN', use_edns
=False)
398 expectedQuery
= dns
.message
.make_query(name
, 'A', 'IN', use_edns
=True, payload
=4096)
400 response
= dns
.message
.make_response(query
)
401 rrset
= dns
.rrset
.from_text(name
,
406 response
.answer
.append(rrset
)
407 (receivedQuery
, receivedResponse
) = self
.sendDOHPostQuery(self
._dohServerPort
, self
._serverName
, self
._dohBaseURL
, query
, response
=response
, caFile
=self
._caCert
)
408 self
.assertTrue(receivedQuery
)
409 self
.assertTrue(receivedResponse
)
410 receivedQuery
.id = expectedQuery
.id
411 self
.assertEquals(expectedQuery
, receivedQuery
)
412 self
.checkQueryEDNSWithoutECS(expectedQuery
, receivedQuery
)
413 self
.assertEquals(response
, receivedResponse
)
415 def testHeaderRule(self
):
419 name
= 'header-rule.doh.tests.powerdns.com.'
420 query
= dns
.message
.make_query(name
, 'A', 'IN')
422 query
.flags
&= ~dns
.flags
.RD
423 expectedResponse
= dns
.message
.make_response(query
)
424 rrset
= dns
.rrset
.from_text(name
,
429 expectedResponse
.answer
.append(rrset
)
431 # this header should match
432 (_
, receivedResponse
) = self
.sendDOHQuery(self
._dohServerPort
, self
._serverName
, self
._dohBaseURL
, caFile
=self
._caCert
, query
=query
, response
=None, useQueue
=False, customHeaders
=['x-powerdnS: aaaaa'])
433 self
.assertEquals(receivedResponse
, expectedResponse
)
435 expectedQuery
= dns
.message
.make_query(name
, 'A', 'IN', use_edns
=True, payload
=4096)
436 expectedQuery
.flags
&= ~dns
.flags
.RD
438 response
= dns
.message
.make_response(query
)
439 rrset
= dns
.rrset
.from_text(name
,
444 response
.answer
.append(rrset
)
446 # this content of the header should NOT match
447 (receivedQuery
, receivedResponse
) = self
.sendDOHQuery(self
._dohServerPort
, self
._serverName
, self
._dohBaseURL
, query
, response
=response
, caFile
=self
._caCert
, customHeaders
=['x-powerdnS: bbbbb'])
448 self
.assertTrue(receivedQuery
)
449 self
.assertTrue(receivedResponse
)
450 receivedQuery
.id = expectedQuery
.id
451 self
.assertEquals(expectedQuery
, receivedQuery
)
452 self
.checkQueryEDNSWithoutECS(expectedQuery
, receivedQuery
)
453 self
.assertEquals(response
, receivedResponse
)
455 def testHTTPPath(self
):
459 name
= 'http-path.doh.tests.powerdns.com.'
460 query
= dns
.message
.make_query(name
, 'A', 'IN')
462 query
.flags
&= ~dns
.flags
.RD
463 expectedResponse
= dns
.message
.make_response(query
)
464 rrset
= dns
.rrset
.from_text(name
,
469 expectedResponse
.answer
.append(rrset
)
471 # this path should match
472 (_
, receivedResponse
) = self
.sendDOHQuery(self
._dohServerPort
, self
._serverName
, self
._dohBaseURL
+ 'PowerDNS', caFile
=self
._caCert
, query
=query
, response
=None, useQueue
=False)
473 self
.assertEquals(receivedResponse
, expectedResponse
)
475 expectedQuery
= dns
.message
.make_query(name
, 'A', 'IN', use_edns
=True, payload
=4096)
477 expectedQuery
.flags
&= ~dns
.flags
.RD
478 response
= dns
.message
.make_response(query
)
479 rrset
= dns
.rrset
.from_text(name
,
484 response
.answer
.append(rrset
)
486 # this path should NOT match
487 (receivedQuery
, receivedResponse
) = self
.sendDOHQuery(self
._dohServerPort
, self
._serverName
, self
._dohBaseURL
+ "PowerDNS2", query
, response
=response
, caFile
=self
._caCert
)
488 self
.assertTrue(receivedQuery
)
489 self
.assertTrue(receivedResponse
)
490 receivedQuery
.id = expectedQuery
.id
491 self
.assertEquals(expectedQuery
, receivedQuery
)
492 self
.checkQueryEDNSWithoutECS(expectedQuery
, receivedQuery
)
493 self
.assertEquals(response
, receivedResponse
)
495 def testHTTPPathRegex(self
):
499 name
= 'http-path-regex.doh.tests.powerdns.com.'
500 query
= dns
.message
.make_query(name
, 'A', 'IN')
502 query
.flags
&= ~dns
.flags
.RD
503 expectedResponse
= dns
.message
.make_response(query
)
504 rrset
= dns
.rrset
.from_text(name
,
509 expectedResponse
.answer
.append(rrset
)
511 # this path should match
512 (_
, receivedResponse
) = self
.sendDOHQuery(self
._dohServerPort
, self
._serverName
, self
._dohBaseURL
+ 'PowerDNS-999', caFile
=self
._caCert
, query
=query
, response
=None, useQueue
=False)
513 self
.assertEquals(receivedResponse
, expectedResponse
)
515 expectedQuery
= dns
.message
.make_query(name
, 'A', 'IN', use_edns
=True, payload
=4096)
517 expectedQuery
.flags
&= ~dns
.flags
.RD
518 response
= dns
.message
.make_response(query
)
519 rrset
= dns
.rrset
.from_text(name
,
524 response
.answer
.append(rrset
)
526 # this path should NOT match
527 (receivedQuery
, receivedResponse
) = self
.sendDOHQuery(self
._dohServerPort
, self
._serverName
, self
._dohBaseURL
+ "PowerDNS2", query
, response
=response
, caFile
=self
._caCert
)
528 self
.assertTrue(receivedQuery
)
529 self
.assertTrue(receivedResponse
)
530 receivedQuery
.id = expectedQuery
.id
531 self
.assertEquals(expectedQuery
, receivedQuery
)
532 self
.checkQueryEDNSWithoutECS(expectedQuery
, receivedQuery
)
533 self
.assertEquals(response
, receivedResponse
)
535 def testHTTPStatusAction200(self
):
537 DOH: HTTPStatusAction 200 OK
539 name
= 'http-status-action.doh.tests.powerdns.com.'
540 query
= dns
.message
.make_query(name
, 'A', 'IN', use_edns
=False)
543 (_
, receivedResponse
) = self
.sendDOHQuery(self
._dohServerPort
, self
._serverName
, self
._dohBaseURL
, query
, caFile
=self
._caCert
, useQueue
=False, rawResponse
=True)
544 self
.assertTrue(receivedResponse
)
545 self
.assertEquals(receivedResponse
, b
'Plaintext answer')
546 self
.assertEquals(self
._rcode
, 200)
547 self
.assertTrue('content-type: text/plain' in self
._response
_headers
.decode())
549 def testHTTPStatusAction307(self
):
551 DOH: HTTPStatusAction 307
553 name
= 'http-status-action-redirect.doh.tests.powerdns.com.'
554 query
= dns
.message
.make_query(name
, 'A', 'IN', use_edns
=False)
557 (_
, receivedResponse
) = self
.sendDOHQuery(self
._dohServerPort
, self
._serverName
, self
._dohBaseURL
, query
, caFile
=self
._caCert
, useQueue
=False, rawResponse
=True)
558 self
.assertTrue(receivedResponse
)
559 self
.assertEquals(self
._rcode
, 307)
560 self
.assertTrue('location: https://doh.powerdns.org' in self
._response
_headers
.decode())
562 def testHTTPLuaResponse(self
):
564 DOH: Lua HTTP Response
566 name
= 'http-lua.doh.tests.powerdns.com.'
567 query
= dns
.message
.make_query(name
, 'A', 'IN', use_edns
=False)
570 (_
, receivedResponse
) = self
.sendDOHPostQuery(self
._dohServerPort
, self
._serverName
, self
._dohBaseURL
, query
, caFile
=self
._caCert
, useQueue
=False, rawResponse
=True)
571 self
.assertTrue(receivedResponse
)
572 self
.assertEquals(receivedResponse
, b
'It works!')
573 self
.assertEquals(self
._rcode
, 200)
574 self
.assertTrue('content-type: text/plain' in self
._response
_headers
.decode())
576 def testHTTPEarlyResponse(self
):
578 DOH: HTTP Early Response
580 response_headers
= BytesIO()
581 url
= self
._dohBaseURL
+ 'coffee'
582 conn
= self
.openDOHConnection(self
._dohServerPort
, caFile
=self
._caCert
, timeout
=2.0)
583 conn
.setopt(pycurl
.URL
, url
)
584 conn
.setopt(pycurl
.RESOLVE
, ["%s:%d:127.0.0.1" % (self
._serverName
, self
._dohServerPort
)])
585 conn
.setopt(pycurl
.SSL_VERIFYPEER
, 1)
586 conn
.setopt(pycurl
.SSL_VERIFYHOST
, 2)
587 conn
.setopt(pycurl
.CAINFO
, self
._caCert
)
588 conn
.setopt(pycurl
.HEADERFUNCTION
, response_headers
.write
)
589 data
= conn
.perform_rb()
590 rcode
= conn
.getinfo(pycurl
.RESPONSE_CODE
)
591 headers
= response_headers
.getvalue().decode()
593 self
.assertEquals(rcode
, 418)
594 self
.assertEquals(data
, b
'C0FFEE')
595 self
.assertIn('foo: bar', headers
)
596 self
.assertNotIn(self
._customResponseHeader
2, headers
)
598 response_headers
= BytesIO()
599 conn
= self
.openDOHConnection(self
._dohServerPort
, caFile
=self
._caCert
, timeout
=2.0)
600 conn
.setopt(pycurl
.URL
, url
)
601 conn
.setopt(pycurl
.RESOLVE
, ["%s:%d:127.0.0.1" % (self
._serverName
, self
._dohServerPort
)])
602 conn
.setopt(pycurl
.SSL_VERIFYPEER
, 1)
603 conn
.setopt(pycurl
.SSL_VERIFYHOST
, 2)
604 conn
.setopt(pycurl
.CAINFO
, self
._caCert
)
605 conn
.setopt(pycurl
.HEADERFUNCTION
, response_headers
.write
)
606 conn
.setopt(pycurl
.POST
, True)
608 conn
.setopt(pycurl
.POSTFIELDS
, data
)
610 data
= conn
.perform_rb()
611 rcode
= conn
.getinfo(pycurl
.RESPONSE_CODE
)
612 headers
= response_headers
.getvalue().decode()
613 self
.assertEquals(rcode
, 418)
614 self
.assertEquals(data
, b
'C0FFEE')
615 self
.assertIn('foo: bar', headers
)
616 self
.assertNotIn(self
._customResponseHeader
2, headers
)
618 class TestDOHAddingECS(DNSDistDOHTest
):
620 _serverKey
= 'server.key'
621 _serverCert
= 'server.chain'
622 _serverName
= 'tls.tests.dnsdist.org'
624 _dohServerPort
= 8443
625 _dohBaseURL
= ("https://%s:%d/" % (_serverName
, _dohServerPort
))
626 _config_template
= """
627 newServer{address="127.0.0.1:%s", useClientSubnet=true}
628 addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" })
631 _config_params
= ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey']
633 def testDOHSimple(self
):
635 DOH with ECS: Simple query
637 name
= 'simple.doh-ecs.tests.powerdns.com.'
638 query
= dns
.message
.make_query(name
, 'A', 'IN', use_edns
=False)
640 rewrittenEcso
= clientsubnetoption
.ClientSubnetOption('127.0.0.0', 24)
641 expectedQuery
= dns
.message
.make_query(name
, 'A', 'IN', use_edns
=True, payload
=4096, options
=[rewrittenEcso
])
642 response
= dns
.message
.make_response(query
)
643 rrset
= dns
.rrset
.from_text(name
,
648 response
.answer
.append(rrset
)
650 (receivedQuery
, receivedResponse
) = self
.sendDOHQuery(self
._dohServerPort
, self
._serverName
, self
._dohBaseURL
, query
, response
=response
, caFile
=self
._caCert
)
651 self
.assertTrue(receivedQuery
)
652 self
.assertTrue(receivedResponse
)
653 expectedQuery
.id = receivedQuery
.id
654 self
.assertEquals(expectedQuery
, receivedQuery
)
655 self
.checkQueryEDNSWithECS(expectedQuery
, receivedQuery
)
656 self
.assertEquals(response
, receivedResponse
)
657 self
.checkResponseNoEDNS(response
, receivedResponse
)
659 def testDOHExistingEDNS(self
):
661 DOH with ECS: Existing EDNS
663 name
= 'existing-edns.doh-ecs.tests.powerdns.com.'
664 query
= dns
.message
.make_query(name
, 'A', 'IN', use_edns
=True, payload
=8192)
666 rewrittenEcso
= clientsubnetoption
.ClientSubnetOption('127.0.0.0', 24)
667 expectedQuery
= dns
.message
.make_query(name
, 'A', 'IN', use_edns
=True, payload
=8192, options
=[rewrittenEcso
])
668 response
= dns
.message
.make_response(query
)
669 rrset
= dns
.rrset
.from_text(name
,
674 response
.answer
.append(rrset
)
676 (receivedQuery
, receivedResponse
) = self
.sendDOHQuery(self
._dohServerPort
, self
._serverName
, self
._dohBaseURL
, query
, response
=response
, caFile
=self
._caCert
)
677 self
.assertTrue(receivedQuery
)
678 self
.assertTrue(receivedResponse
)
679 receivedQuery
.id = expectedQuery
.id
680 self
.assertEquals(expectedQuery
, receivedQuery
)
681 self
.assertEquals(response
, receivedResponse
)
682 self
.checkQueryEDNSWithECS(expectedQuery
, receivedQuery
)
683 self
.checkResponseEDNSWithoutECS(response
, receivedResponse
)
685 def testDOHExistingECS(self
):
687 DOH with ECS: Existing EDNS Client Subnet
689 name
= 'existing-ecs.doh-ecs.tests.powerdns.com.'
690 ecso
= clientsubnetoption
.ClientSubnetOption('1.2.3.4')
691 rewrittenEcso
= clientsubnetoption
.ClientSubnetOption('127.0.0.0', 24)
692 query
= dns
.message
.make_query(name
, 'A', 'IN', use_edns
=True, payload
=512, options
=[ecso
], want_dnssec
=True)
694 expectedQuery
= dns
.message
.make_query(name
, 'A', 'IN', use_edns
=True, payload
=512, options
=[rewrittenEcso
])
695 response
= dns
.message
.make_response(query
)
696 response
.use_edns(edns
=True, payload
=4096, options
=[rewrittenEcso
])
697 rrset
= dns
.rrset
.from_text(name
,
702 response
.answer
.append(rrset
)
704 (receivedQuery
, receivedResponse
) = self
.sendDOHQuery(self
._dohServerPort
, self
._serverName
, self
._dohBaseURL
, query
, response
=response
, caFile
=self
._caCert
)
705 self
.assertTrue(receivedQuery
)
706 self
.assertTrue(receivedResponse
)
707 receivedQuery
.id = expectedQuery
.id
708 self
.assertEquals(expectedQuery
, receivedQuery
)
709 self
.assertEquals(response
, receivedResponse
)
710 self
.checkQueryEDNSWithECS(expectedQuery
, receivedQuery
)
711 self
.checkResponseEDNSWithECS(response
, receivedResponse
)
713 class TestDOHOverHTTP(DNSDistDOHTest
):
715 _dohServerPort
= 8480
716 _serverName
= 'tls.tests.dnsdist.org'
717 _dohBaseURL
= ("http://%s:%d/" % (_serverName
, _dohServerPort
))
718 _config_template
= """
719 newServer{address="127.0.0.1:%s"}
720 addDOHLocal("127.0.0.1:%s")
722 _config_params
= ['_testServerPort', '_dohServerPort']
723 _checkConfigExpectedOutput
= b
"""No certificate provided for DoH endpoint 127.0.0.1:8480, running in DNS over HTTP mode instead of DNS over HTTPS
724 Configuration 'configs/dnsdist_TestDOHOverHTTP.conf' OK!
727 def testDOHSimple(self
):
729 DOH over HTTP: Simple query
731 name
= 'simple.doh-over-http.tests.powerdns.com.'
732 query
= dns
.message
.make_query(name
, 'A', 'IN', use_edns
=False)
734 expectedQuery
= dns
.message
.make_query(name
, 'A', 'IN', use_edns
=True, payload
=4096)
735 response
= dns
.message
.make_response(query
)
736 rrset
= dns
.rrset
.from_text(name
,
741 response
.answer
.append(rrset
)
743 (receivedQuery
, receivedResponse
) = self
.sendDOHQuery(self
._dohServerPort
, self
._serverName
, self
._dohBaseURL
, query
, response
=response
, useHTTPS
=False)
744 self
.assertTrue(receivedQuery
)
745 self
.assertTrue(receivedResponse
)
746 expectedQuery
.id = receivedQuery
.id
747 self
.assertEquals(expectedQuery
, receivedQuery
)
748 self
.checkQueryEDNSWithoutECS(expectedQuery
, receivedQuery
)
749 self
.assertEquals(response
, receivedResponse
)
750 self
.checkResponseNoEDNS(response
, receivedResponse
)
752 def testDOHSimplePOST(self
):
754 DOH over HTTP: Simple POST query
756 name
= 'simple-post.doh-over-http.tests.powerdns.com.'
757 query
= dns
.message
.make_query(name
, 'A', 'IN', use_edns
=False)
759 expectedQuery
= dns
.message
.make_query(name
, 'A', 'IN', use_edns
=True, payload
=4096)
761 response
= dns
.message
.make_response(query
)
762 rrset
= dns
.rrset
.from_text(name
,
767 response
.answer
.append(rrset
)
769 (receivedQuery
, receivedResponse
) = self
.sendDOHPostQuery(self
._dohServerPort
, self
._serverName
, self
._dohBaseURL
, query
, response
=response
, useHTTPS
=False)
770 self
.assertTrue(receivedQuery
)
771 self
.assertTrue(receivedResponse
)
772 receivedQuery
.id = expectedQuery
.id
773 self
.assertEquals(expectedQuery
, receivedQuery
)
774 self
.checkQueryEDNSWithoutECS(expectedQuery
, receivedQuery
)
775 self
.assertEquals(response
, receivedResponse
)
776 self
.checkResponseNoEDNS(response
, receivedResponse
)
778 class TestDOHWithCache(DNSDistDOHTest
):
780 _serverKey
= 'server.key'
781 _serverCert
= 'server.chain'
782 _serverName
= 'tls.tests.dnsdist.org'
784 _dohServerPort
= 8443
785 _dohBaseURL
= ("https://%s:%d/" % (_serverName
, _dohServerPort
))
786 _config_template
= """
787 newServer{address="127.0.0.1:%s"}
789 addDOHLocal("127.0.0.1:%s", "%s", "%s")
791 pc = newPacketCache(100, {maxTTL=86400, minTTL=1})
792 getPool(""):setCache(pc)
794 _config_params
= ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey']
796 def testDOHCacheLargeAnswer(self
):
798 DOH with cache: Check that we can cache (and retrieve) large answers
801 name
= 'large.doh-with-cache.tests.powerdns.com.'
802 query
= dns
.message
.make_query(name
, 'A', 'IN', use_edns
=False)
804 expectedQuery
= dns
.message
.make_query(name
, 'A', 'IN', use_edns
=True, payload
=4096)
806 response
= dns
.message
.make_response(query
)
807 # we prepare a large answer
811 content
= content
+ ', '
812 content
= content
+ (str(i
)*50)
814 content
= content
+ 'A'*40
816 rrset
= dns
.rrset
.from_text(name
,
821 response
.answer
.append(rrset
)
822 self
.assertEquals(len(response
.to_wire()), 4096)
824 # first query to fill the cache
825 (receivedQuery
, receivedResponse
) = self
.sendDOHQuery(self
._dohServerPort
, self
._serverName
, self
._dohBaseURL
, query
, response
=response
, caFile
=self
._caCert
)
826 self
.assertTrue(receivedQuery
)
827 self
.assertTrue(receivedResponse
)
828 receivedQuery
.id = expectedQuery
.id
829 self
.assertEquals(expectedQuery
, receivedQuery
)
830 self
.checkQueryEDNSWithoutECS(expectedQuery
, receivedQuery
)
831 self
.assertEquals(response
, receivedResponse
)
832 self
.checkHasHeader('cache-control', 'max-age=3600')
834 for _
in range(numberOfQueries
):
835 (_
, receivedResponse
) = self
.sendDOHQuery(self
._dohServerPort
, self
._serverName
, self
._dohBaseURL
, query
, caFile
=self
._caCert
, useQueue
=False)
836 self
.assertEquals(receivedResponse
, response
)
837 self
.checkHasHeader('cache-control', 'max-age=' + str(receivedResponse
.answer
[0].ttl
))
841 (_
, receivedResponse
) = self
.sendDOHQuery(self
._dohServerPort
, self
._serverName
, self
._dohBaseURL
, query
, caFile
=self
._caCert
, useQueue
=False)
842 self
.assertEquals(receivedResponse
, response
)
843 self
.checkHasHeader('cache-control', 'max-age=' + str(receivedResponse
.answer
[0].ttl
))
845 class TestDOHWithoutCacheControl(DNSDistDOHTest
):
847 _serverKey
= 'server.key'
848 _serverCert
= 'server.chain'
849 _serverName
= 'tls.tests.dnsdist.org'
851 _dohServerPort
= 8443
852 _dohBaseURL
= ("https://%s:%d/" % (_serverName
, _dohServerPort
))
853 _config_template
= """
854 newServer{address="127.0.0.1:%s"}
856 addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" }, {sendCacheControlHeaders=false})
858 _config_params
= ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey']
860 def testDOHSimple(self
):
862 DOH without cache-control
864 name
= 'simple.doh.tests.powerdns.com.'
865 query
= dns
.message
.make_query(name
, 'A', 'IN', use_edns
=False)
867 expectedQuery
= dns
.message
.make_query(name
, 'A', 'IN', use_edns
=True, payload
=4096)
869 response
= dns
.message
.make_response(query
)
870 rrset
= dns
.rrset
.from_text(name
,
875 response
.answer
.append(rrset
)
877 (receivedQuery
, receivedResponse
) = self
.sendDOHQuery(self
._dohServerPort
, self
._serverName
, self
._dohBaseURL
, query
, response
=response
, caFile
=self
._caCert
)
878 self
.assertTrue(receivedQuery
)
879 self
.assertTrue(receivedResponse
)
880 receivedQuery
.id = expectedQuery
.id
881 self
.assertEquals(expectedQuery
, receivedQuery
)
882 self
.checkNoHeader('cache-control')
883 self
.checkQueryEDNSWithoutECS(expectedQuery
, receivedQuery
)
884 self
.assertEquals(response
, receivedResponse
)
886 class TestDOHFFI(DNSDistDOHTest
):
888 _serverKey
= 'server.key'
889 _serverCert
= 'server.chain'
890 _serverName
= 'tls.tests.dnsdist.org'
892 _dohServerPort
= 8443
893 _customResponseHeader1
= 'access-control-allow-origin: *'
894 _customResponseHeader2
= 'user-agent: derp'
895 _dohBaseURL
= ("https://%s:%d/" % (_serverName
, _dohServerPort
))
896 _config_template
= """
897 newServer{address="127.0.0.1:%s"}
899 addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" }, {customResponseHeaders={["access-control-allow-origin"]="*",["user-agent"]="derp",["UPPERCASE"]="VaLuE"}})
901 local ffi = require("ffi")
903 function dohHandler(dq)
904 local scheme = ffi.string(ffi.C.dnsdist_ffi_dnsquestion_get_http_scheme(dq))
905 local host = ffi.string(ffi.C.dnsdist_ffi_dnsquestion_get_http_host(dq))
906 local path = ffi.string(ffi.C.dnsdist_ffi_dnsquestion_get_http_path(dq))
907 local query_string = ffi.string(ffi.C.dnsdist_ffi_dnsquestion_get_http_query_string(dq))
908 if scheme == 'https' and host == '%s:%d' and path == '/' and query_string == '' then
909 local foundct = false
910 local headers_ptr = ffi.new("const dnsdist_http_header_t *[1]")
911 local headers_ptr_param = ffi.cast("const dnsdist_http_header_t **", headers_ptr)
913 local headers_count = tonumber(ffi.C.dnsdist_ffi_dnsquestion_get_http_headers(dq, headers_ptr_param))
914 if headers_count > 0 then
915 for idx = 0, headers_count-1 do
916 if ffi.string(headers_ptr[0][idx].name) == 'content-type' and ffi.string(headers_ptr[0][idx].value) == 'application/dns-message' then
923 ffi.C.dnsdist_ffi_dnsquestion_set_http_response(dq, 200, 'It works!', 'text/plain')
924 return DNSAction.HeaderModify
927 return DNSAction.None
929 addAction("http-lua-ffi.doh.tests.powerdns.com.", LuaFFIAction(dohHandler))
931 _config_params
= ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey', '_serverName', '_dohServerPort']
933 def testHTTPLuaFFIResponse(self
):
935 DOH: Lua FFI HTTP Response
937 name
= 'http-lua-ffi.doh.tests.powerdns.com.'
938 query
= dns
.message
.make_query(name
, 'A', 'IN', use_edns
=False)
941 (_
, receivedResponse
) = self
.sendDOHPostQuery(self
._dohServerPort
, self
._serverName
, self
._dohBaseURL
, query
, caFile
=self
._caCert
, useQueue
=False, rawResponse
=True)
942 self
.assertTrue(receivedResponse
)
943 self
.assertEquals(receivedResponse
, b
'It works!')
944 self
.assertEquals(self
._rcode
, 200)
945 self
.assertTrue('content-type: text/plain' in self
._response
_headers
.decode())