]> git.ipfire.org Git - thirdparty/pdns.git/blob - regression-tests.dnsdist/test_DOH.py
dnsdist: Don't accept sub-paths of configured DoH URLs
[thirdparty/pdns.git] / regression-tests.dnsdist / test_DOH.py
1 #!/usr/bin/env python
2 import base64
3 import dns
4 import os
5 import unittest
6 import clientsubnetoption
7 from dnsdisttests import DNSDistTest
8
9 import pycurl
10 from io import BytesIO
11 #from hyper import HTTP20Connection
12 #from hyper.ssl_compat import SSLContext, PROTOCOL_TLSv1_2
13
14 @unittest.skipIf('SKIP_DOH_TESTS' in os.environ, 'DNS over HTTPS tests are disabled')
15 class DNSDistDOHTest(DNSDistTest):
16
17 @classmethod
18 def getDOHGetURL(cls, baseurl, query, rawQuery=False):
19 if rawQuery:
20 wire = query
21 else:
22 wire = query.to_wire()
23 param = base64.urlsafe_b64encode(wire).decode('UTF8').rstrip('=')
24 return baseurl + "?dns=" + param
25
26 @classmethod
27 def openDOHConnection(cls, port, caFile, timeout=2.0):
28 conn = pycurl.Curl()
29 conn.setopt(pycurl.HTTP_VERSION, pycurl.CURL_HTTP_VERSION_2)
30
31 conn.setopt(pycurl.HTTPHEADER, ["Content-type: application/dns-message",
32 "Accept: application/dns-message"])
33 return conn
34
35 @classmethod
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)])
43 if useHTTPS:
44 conn.setopt(pycurl.SSL_VERIFYPEER, 1)
45 conn.setopt(pycurl.SSL_VERIFYHOST, 2)
46 if caFile:
47 conn.setopt(pycurl.CAINFO, caFile)
48
49 conn.setopt(pycurl.HTTPHEADER, customHeaders)
50 conn.setopt(pycurl.HEADERFUNCTION, response_headers.write)
51
52 if response:
53 cls._toResponderQueue.put(response, True, timeout)
54
55 receivedQuery = None
56 message = None
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)
62 elif rawResponse:
63 message = data
64
65 if useQueue and not cls._fromResponderQueue.empty():
66 receivedQuery = cls._fromResponderQueue.get(True, timeout)
67
68 cls._response_headers = response_headers.getvalue()
69 return (receivedQuery, message)
70
71 @classmethod
72 def sendDOHPostQuery(cls, port, servername, baseurl, query, response=None, timeout=2.0, caFile=None, useQueue=True, rawQuery=False, rawResponse=False, customHeaders=[], useHTTPS=True):
73 url = baseurl
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)])
79 if useHTTPS:
80 conn.setopt(pycurl.SSL_VERIFYPEER, 1)
81 conn.setopt(pycurl.SSL_VERIFYHOST, 2)
82 if caFile:
83 conn.setopt(pycurl.CAINFO, caFile)
84
85 conn.setopt(pycurl.HTTPHEADER, customHeaders)
86 conn.setopt(pycurl.HEADERFUNCTION, response_headers.write)
87 conn.setopt(pycurl.POST, True)
88 data = query
89 if not rawQuery:
90 data = data.to_wire()
91
92 conn.setopt(pycurl.POSTFIELDS, data)
93
94 if response:
95 cls._toResponderQueue.put(response, True, timeout)
96
97 receivedQuery = None
98 message = None
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)
104 elif rawResponse:
105 message = data
106
107 if useQueue and not cls._fromResponderQueue.empty():
108 receivedQuery = cls._fromResponderQueue.get(True, timeout)
109
110 cls._response_headers = response_headers.getvalue()
111 return (receivedQuery, message)
112
113 @classmethod
114 def setUpClass(cls):
115
116 # for some reason, @unittest.skipIf() is not applied to derived classes with some versions of Python
117 if 'SKIP_DOH_TESTS' in os.environ:
118 raise unittest.SkipTest('DNS over HTTPS tests are disabled')
119
120 cls.startResponders()
121 cls.startDNSDist()
122 cls.setUpSockets()
123
124 print("Launching tests..")
125
126 # @classmethod
127 # def openDOHConnection(cls, port, caFile, timeout=2.0):
128 # sslctx = SSLContext(PROTOCOL_TLSv1_2)
129 # sslctx.load_verify_locations(caFile)
130 # return HTTP20Connection('127.0.0.1', port=port, secure=True, timeout=timeout, ssl_context=sslctx, force_proto='h2')
131
132 # @classmethod
133 # def sendDOHQueryOverConnection(cls, conn, baseurl, query, response=None, timeout=2.0):
134 # url = cls.getDOHGetURL(baseurl, query)
135
136 # if response:
137 # cls._toResponderQueue.put(response, True, timeout)
138
139 # conn.request('GET', url)
140
141 # @classmethod
142 # def recvDOHResponseOverConnection(cls, conn, useQueue=False, timeout=2.0):
143 # message = None
144 # data = conn.get_response()
145 # if data:
146 # data = data.read()
147 # if data:
148 # message = dns.message.from_wire(data)
149
150 # if useQueue and not cls._fromResponderQueue.empty():
151 # receivedQuery = cls._fromResponderQueue.get(True, timeout)
152 # return (receivedQuery, message)
153 # else:
154 # return message
155
156 class TestDOH(DNSDistDOHTest):
157
158 _serverKey = 'server.key'
159 _serverCert = 'server.chain'
160 _serverName = 'tls.tests.dnsdist.org'
161 _caCert = 'ca.pem'
162 _dohServerPort = 8443
163 _customResponseHeader1 = 'access-control-allow-origin: *'
164 _customResponseHeader2 = 'user-agent: derp'
165 _dohBaseURL = ("https://%s:%d/" % (_serverName, _dohServerPort))
166 _config_template = """
167 newServer{address="127.0.0.1:%s"}
168
169 addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/", "/coffee", "/PowerDNS", "/PowerDNS2", "/PowerDNS-999" }, {customResponseHeaders={["access-control-allow-origin"]="*",["user-agent"]="derp",["UPPERCASE"]="VaLuE"}})
170 dohFE = getDOHFrontend(0)
171 dohFE:setResponsesMap({newDOHResponseMapEntry('^/coffee$', 418, 'C0FFEE', {['FoO']='bar'})})
172
173 addAction("drop.doh.tests.powerdns.com.", DropAction())
174 addAction("refused.doh.tests.powerdns.com.", RCodeAction(DNSRCode.REFUSED))
175 addAction("spoof.doh.tests.powerdns.com.", SpoofAction("1.2.3.4"))
176 addAction(HTTPHeaderRule("X-PowerDNS", "^[a]{5}$"), SpoofAction("2.3.4.5"))
177 addAction(HTTPPathRule("/PowerDNS"), SpoofAction("3.4.5.6"))
178 addAction(HTTPPathRegexRule("^/PowerDNS-[0-9]"), SpoofAction("6.7.8.9"))
179 addAction("http-status-action.doh.tests.powerdns.com.", HTTPStatusAction(200, "Plaintext answer", "text/plain"))
180 addAction("http-status-action-redirect.doh.tests.powerdns.com.", HTTPStatusAction(307, "https://doh.powerdns.org"))
181
182 function dohHandler(dq)
183 if dq:getHTTPScheme() == 'https' and dq:getHTTPHost() == '%s:%d' and dq:getHTTPPath() == '/' and dq:getHTTPQueryString() == '' then
184 local foundct = false
185 for key,value in pairs(dq:getHTTPHeaders()) do
186 if key == 'content-type' and value == 'application/dns-message' then
187 foundct = true
188 break
189 end
190 end
191 if foundct then
192 dq:setHTTPResponse(200, 'It works!', 'text/plain')
193 dq.dh:setQR(true)
194 return DNSAction.HeaderModify
195 end
196 end
197 return DNSAction.None
198 end
199 addAction("http-lua.doh.tests.powerdns.com.", LuaAction(dohHandler))
200 """
201 _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey', '_serverName', '_dohServerPort']
202
203 def testDOHSimple(self):
204 """
205 DOH: Simple query
206 """
207 name = 'simple.doh.tests.powerdns.com.'
208 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
209 query.id = 0
210 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
211 expectedQuery.id = 0
212 response = dns.message.make_response(query)
213 rrset = dns.rrset.from_text(name,
214 3600,
215 dns.rdataclass.IN,
216 dns.rdatatype.A,
217 '127.0.0.1')
218 response.answer.append(rrset)
219
220 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert)
221 self.assertTrue(receivedQuery)
222 self.assertTrue(receivedResponse)
223 receivedQuery.id = expectedQuery.id
224 self.assertEquals(expectedQuery, receivedQuery)
225 self.assertTrue((self._customResponseHeader1) in self._response_headers.decode())
226 self.assertTrue((self._customResponseHeader2) in self._response_headers.decode())
227 self.assertFalse(('UPPERCASE: VaLuE' in self._response_headers.decode()))
228 self.assertTrue(('uppercase: VaLuE' in self._response_headers.decode()))
229 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
230 self.assertEquals(response, receivedResponse)
231
232 def testDOHSimplePOST(self):
233 """
234 DOH: Simple POST query
235 """
236 name = 'simple-post.doh.tests.powerdns.com.'
237 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
238 query.id = 0
239 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
240 expectedQuery.id = 0
241 response = dns.message.make_response(query)
242 rrset = dns.rrset.from_text(name,
243 3600,
244 dns.rdataclass.IN,
245 dns.rdatatype.A,
246 '127.0.0.1')
247 response.answer.append(rrset)
248
249 (receivedQuery, receivedResponse) = self.sendDOHPostQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert)
250 self.assertTrue(receivedQuery)
251 self.assertTrue(receivedResponse)
252 receivedQuery.id = expectedQuery.id
253 self.assertEquals(expectedQuery, receivedQuery)
254 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
255 self.assertEquals(response, receivedResponse)
256
257 def testDOHExistingEDNS(self):
258 """
259 DOH: Existing EDNS
260 """
261 name = 'existing-edns.doh.tests.powerdns.com.'
262 query = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=8192)
263 query.id = 0
264 response = dns.message.make_response(query)
265 rrset = dns.rrset.from_text(name,
266 3600,
267 dns.rdataclass.IN,
268 dns.rdatatype.A,
269 '127.0.0.1')
270 response.answer.append(rrset)
271
272 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert)
273 self.assertTrue(receivedQuery)
274 self.assertTrue(receivedResponse)
275 receivedQuery.id = query.id
276 self.assertEquals(query, receivedQuery)
277 self.assertEquals(response, receivedResponse)
278 self.checkQueryEDNSWithoutECS(query, receivedQuery)
279 self.checkResponseEDNSWithoutECS(response, receivedResponse)
280
281 def testDOHExistingECS(self):
282 """
283 DOH: Existing EDNS Client Subnet
284 """
285 name = 'existing-ecs.doh.tests.powerdns.com.'
286 ecso = clientsubnetoption.ClientSubnetOption('1.2.3.4')
287 rewrittenEcso = clientsubnetoption.ClientSubnetOption('127.0.0.1', 24)
288 query = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=512, options=[ecso], want_dnssec=True)
289 query.id = 0
290 response = dns.message.make_response(query)
291 response.use_edns(edns=True, payload=4096, options=[rewrittenEcso])
292 rrset = dns.rrset.from_text(name,
293 3600,
294 dns.rdataclass.IN,
295 dns.rdatatype.A,
296 '127.0.0.1')
297 response.answer.append(rrset)
298
299 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert)
300 self.assertTrue(receivedQuery)
301 self.assertTrue(receivedResponse)
302 receivedQuery.id = query.id
303 self.assertEquals(query, receivedQuery)
304 self.assertEquals(response, receivedResponse)
305 self.checkQueryEDNSWithECS(query, receivedQuery)
306 self.checkResponseEDNSWithECS(response, receivedResponse)
307
308 def testDropped(self):
309 """
310 DOH: Dropped query
311 """
312 name = 'drop.doh.tests.powerdns.com.'
313 query = dns.message.make_query(name, 'A', 'IN')
314 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, caFile=self._caCert, query=query, response=None, useQueue=False)
315 self.assertEquals(receivedResponse, None)
316
317 def testRefused(self):
318 """
319 DOH: Refused
320 """
321 name = 'refused.doh.tests.powerdns.com.'
322 query = dns.message.make_query(name, 'A', 'IN')
323 query.id = 0
324 query.flags &= ~dns.flags.RD
325 expectedResponse = dns.message.make_response(query)
326 expectedResponse.set_rcode(dns.rcode.REFUSED)
327
328 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, caFile=self._caCert, query=query, response=None, useQueue=False)
329 self.assertEquals(receivedResponse, expectedResponse)
330
331 def testSpoof(self):
332 """
333 DOH: Spoofed
334 """
335 name = 'spoof.doh.tests.powerdns.com.'
336 query = dns.message.make_query(name, 'A', 'IN')
337 query.id = 0
338 query.flags &= ~dns.flags.RD
339 expectedResponse = dns.message.make_response(query)
340 rrset = dns.rrset.from_text(name,
341 3600,
342 dns.rdataclass.IN,
343 dns.rdatatype.A,
344 '1.2.3.4')
345 expectedResponse.answer.append(rrset)
346
347 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, caFile=self._caCert, query=query, response=None, useQueue=False)
348 self.assertEquals(receivedResponse, expectedResponse)
349
350 def testDOHInvalid(self):
351 """
352 DOH: Invalid query
353 """
354 name = 'invalid.doh.tests.powerdns.com.'
355 invalidQuery = dns.message.make_query(name, 'A', 'IN', use_edns=False)
356 invalidQuery.id = 0
357 # first an invalid query
358 invalidQuery = invalidQuery.to_wire()
359 invalidQuery = invalidQuery[:-5]
360 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, caFile=self._caCert, query=invalidQuery, response=None, useQueue=False, rawQuery=True)
361 self.assertEquals(receivedResponse, None)
362
363 # and now a valid one
364 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
365 query.id = 0
366 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
367 expectedQuery.id = 0
368 response = dns.message.make_response(query)
369 rrset = dns.rrset.from_text(name,
370 3600,
371 dns.rdataclass.IN,
372 dns.rdatatype.A,
373 '127.0.0.1')
374 response.answer.append(rrset)
375 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert)
376 self.assertTrue(receivedQuery)
377 self.assertTrue(receivedResponse)
378 receivedQuery.id = expectedQuery.id
379 self.assertEquals(expectedQuery, receivedQuery)
380 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
381 self.assertEquals(response, receivedResponse)
382
383 def testDOHWithoutQuery(self):
384 """
385 DOH: Empty GET query
386 """
387 name = 'empty-get.doh.tests.powerdns.com.'
388 url = self._dohBaseURL
389 conn = self.openDOHConnection(self._dohServerPort, self._caCert, timeout=2.0)
390 conn.setopt(pycurl.URL, url)
391 conn.setopt(pycurl.RESOLVE, ["%s:%d:127.0.0.1" % (self._serverName, self._dohServerPort)])
392 conn.setopt(pycurl.SSL_VERIFYPEER, 1)
393 conn.setopt(pycurl.SSL_VERIFYHOST, 2)
394 conn.setopt(pycurl.CAINFO, self._caCert)
395 data = conn.perform_rb()
396 rcode = conn.getinfo(pycurl.RESPONSE_CODE)
397 self.assertEquals(rcode, 400)
398
399 def testDOHEmptyPOST(self):
400 """
401 DOH: Empty POST query
402 """
403 name = 'empty-post.doh.tests.powerdns.com.'
404
405 (_, receivedResponse) = self.sendDOHPostQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query="", rawQuery=True, response=None, caFile=self._caCert)
406 self.assertEquals(receivedResponse, None)
407
408 # and now a valid one
409 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
410 query.id = 0
411 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
412 expectedQuery.id = 0
413 response = dns.message.make_response(query)
414 rrset = dns.rrset.from_text(name,
415 3600,
416 dns.rdataclass.IN,
417 dns.rdatatype.A,
418 '127.0.0.1')
419 response.answer.append(rrset)
420 (receivedQuery, receivedResponse) = self.sendDOHPostQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert)
421 self.assertTrue(receivedQuery)
422 self.assertTrue(receivedResponse)
423 receivedQuery.id = expectedQuery.id
424 self.assertEquals(expectedQuery, receivedQuery)
425 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
426 self.assertEquals(response, receivedResponse)
427
428 def testHeaderRule(self):
429 """
430 DOH: HeaderRule
431 """
432 name = 'header-rule.doh.tests.powerdns.com.'
433 query = dns.message.make_query(name, 'A', 'IN')
434 query.id = 0
435 query.flags &= ~dns.flags.RD
436 expectedResponse = dns.message.make_response(query)
437 rrset = dns.rrset.from_text(name,
438 3600,
439 dns.rdataclass.IN,
440 dns.rdatatype.A,
441 '2.3.4.5')
442 expectedResponse.answer.append(rrset)
443
444 # this header should match
445 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, caFile=self._caCert, query=query, response=None, useQueue=False, customHeaders=['x-powerdnS: aaaaa'])
446 self.assertEquals(receivedResponse, expectedResponse)
447
448 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
449 expectedQuery.flags &= ~dns.flags.RD
450 expectedQuery.id = 0
451 response = dns.message.make_response(query)
452 rrset = dns.rrset.from_text(name,
453 3600,
454 dns.rdataclass.IN,
455 dns.rdatatype.A,
456 '127.0.0.1')
457 response.answer.append(rrset)
458
459 # this content of the header should NOT match
460 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert, customHeaders=['x-powerdnS: bbbbb'])
461 self.assertTrue(receivedQuery)
462 self.assertTrue(receivedResponse)
463 receivedQuery.id = expectedQuery.id
464 self.assertEquals(expectedQuery, receivedQuery)
465 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
466 self.assertEquals(response, receivedResponse)
467
468 def testHTTPPath(self):
469 """
470 DOH: HTTPPath
471 """
472 name = 'http-path.doh.tests.powerdns.com.'
473 query = dns.message.make_query(name, 'A', 'IN')
474 query.id = 0
475 query.flags &= ~dns.flags.RD
476 expectedResponse = dns.message.make_response(query)
477 rrset = dns.rrset.from_text(name,
478 3600,
479 dns.rdataclass.IN,
480 dns.rdatatype.A,
481 '3.4.5.6')
482 expectedResponse.answer.append(rrset)
483
484 # this path should match
485 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL + 'PowerDNS', caFile=self._caCert, query=query, response=None, useQueue=False)
486 self.assertEquals(receivedResponse, expectedResponse)
487
488 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
489 expectedQuery.id = 0
490 expectedQuery.flags &= ~dns.flags.RD
491 response = dns.message.make_response(query)
492 rrset = dns.rrset.from_text(name,
493 3600,
494 dns.rdataclass.IN,
495 dns.rdatatype.A,
496 '127.0.0.1')
497 response.answer.append(rrset)
498
499 # this path should NOT match
500 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL + "PowerDNS2", query, response=response, caFile=self._caCert)
501 self.assertTrue(receivedQuery)
502 self.assertTrue(receivedResponse)
503 receivedQuery.id = expectedQuery.id
504 self.assertEquals(expectedQuery, receivedQuery)
505 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
506 self.assertEquals(response, receivedResponse)
507
508 # this path is not in the URLs map and should lead to a 404
509 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL + "PowerDNS/something", query, caFile=self._caCert, useQueue=False, rawResponse=True)
510 self.assertTrue(receivedResponse)
511 self.assertEquals(receivedResponse, b'there is no endpoint configured for this path')
512 self.assertEquals(self._rcode, 404)
513
514 def testHTTPPathRegex(self):
515 """
516 DOH: HTTPPathRegex
517 """
518 name = 'http-path-regex.doh.tests.powerdns.com.'
519 query = dns.message.make_query(name, 'A', 'IN')
520 query.id = 0
521 query.flags &= ~dns.flags.RD
522 expectedResponse = dns.message.make_response(query)
523 rrset = dns.rrset.from_text(name,
524 3600,
525 dns.rdataclass.IN,
526 dns.rdatatype.A,
527 '6.7.8.9')
528 expectedResponse.answer.append(rrset)
529
530 # this path should match
531 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL + 'PowerDNS-999', caFile=self._caCert, query=query, response=None, useQueue=False)
532 self.assertEquals(receivedResponse, expectedResponse)
533
534 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
535 expectedQuery.id = 0
536 expectedQuery.flags &= ~dns.flags.RD
537 response = dns.message.make_response(query)
538 rrset = dns.rrset.from_text(name,
539 3600,
540 dns.rdataclass.IN,
541 dns.rdatatype.A,
542 '127.0.0.1')
543 response.answer.append(rrset)
544
545 # this path should NOT match
546 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL + "PowerDNS2", query, response=response, caFile=self._caCert)
547 self.assertTrue(receivedQuery)
548 self.assertTrue(receivedResponse)
549 receivedQuery.id = expectedQuery.id
550 self.assertEquals(expectedQuery, receivedQuery)
551 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
552 self.assertEquals(response, receivedResponse)
553
554 def testHTTPStatusAction200(self):
555 """
556 DOH: HTTPStatusAction 200 OK
557 """
558 name = 'http-status-action.doh.tests.powerdns.com.'
559 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
560 query.id = 0
561
562 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, caFile=self._caCert, useQueue=False, rawResponse=True)
563 self.assertTrue(receivedResponse)
564 self.assertEquals(receivedResponse, b'Plaintext answer')
565 self.assertEquals(self._rcode, 200)
566 self.assertTrue('content-type: text/plain' in self._response_headers.decode())
567
568 def testHTTPStatusAction307(self):
569 """
570 DOH: HTTPStatusAction 307
571 """
572 name = 'http-status-action-redirect.doh.tests.powerdns.com.'
573 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
574 query.id = 0
575
576 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, caFile=self._caCert, useQueue=False, rawResponse=True)
577 self.assertTrue(receivedResponse)
578 self.assertEquals(self._rcode, 307)
579 self.assertTrue('location: https://doh.powerdns.org' in self._response_headers.decode())
580
581 def testHTTPLuaResponse(self):
582 """
583 DOH: Lua HTTP Response
584 """
585 name = 'http-lua.doh.tests.powerdns.com.'
586 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
587 query.id = 0
588
589 (_, receivedResponse) = self.sendDOHPostQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, caFile=self._caCert, useQueue=False, rawResponse=True)
590 self.assertTrue(receivedResponse)
591 self.assertEquals(receivedResponse, b'It works!')
592 self.assertEquals(self._rcode, 200)
593 self.assertTrue('content-type: text/plain' in self._response_headers.decode())
594
595 def testHTTPEarlyResponse(self):
596 """
597 DOH: HTTP Early Response
598 """
599 response_headers = BytesIO()
600 url = self._dohBaseURL + 'coffee'
601 conn = self.openDOHConnection(self._dohServerPort, caFile=self._caCert, timeout=2.0)
602 conn.setopt(pycurl.URL, url)
603 conn.setopt(pycurl.RESOLVE, ["%s:%d:127.0.0.1" % (self._serverName, self._dohServerPort)])
604 conn.setopt(pycurl.SSL_VERIFYPEER, 1)
605 conn.setopt(pycurl.SSL_VERIFYHOST, 2)
606 conn.setopt(pycurl.CAINFO, self._caCert)
607 conn.setopt(pycurl.HEADERFUNCTION, response_headers.write)
608 data = conn.perform_rb()
609 rcode = conn.getinfo(pycurl.RESPONSE_CODE)
610 headers = response_headers.getvalue().decode()
611
612 self.assertEquals(rcode, 418)
613 self.assertEquals(data, b'C0FFEE')
614 self.assertIn('foo: bar', headers)
615 self.assertNotIn(self._customResponseHeader2, headers)
616
617 response_headers = BytesIO()
618 conn = self.openDOHConnection(self._dohServerPort, caFile=self._caCert, timeout=2.0)
619 conn.setopt(pycurl.URL, url)
620 conn.setopt(pycurl.RESOLVE, ["%s:%d:127.0.0.1" % (self._serverName, self._dohServerPort)])
621 conn.setopt(pycurl.SSL_VERIFYPEER, 1)
622 conn.setopt(pycurl.SSL_VERIFYHOST, 2)
623 conn.setopt(pycurl.CAINFO, self._caCert)
624 conn.setopt(pycurl.HEADERFUNCTION, response_headers.write)
625 conn.setopt(pycurl.POST, True)
626 data = ''
627 conn.setopt(pycurl.POSTFIELDS, data)
628
629 data = conn.perform_rb()
630 rcode = conn.getinfo(pycurl.RESPONSE_CODE)
631 headers = response_headers.getvalue().decode()
632 self.assertEquals(rcode, 418)
633 self.assertEquals(data, b'C0FFEE')
634 self.assertIn('foo: bar', headers)
635 self.assertNotIn(self._customResponseHeader2, headers)
636
637 class TestDOHAddingECS(DNSDistDOHTest):
638
639 _serverKey = 'server.key'
640 _serverCert = 'server.chain'
641 _serverName = 'tls.tests.dnsdist.org'
642 _caCert = 'ca.pem'
643 _dohServerPort = 8443
644 _dohBaseURL = ("https://%s:%d/" % (_serverName, _dohServerPort))
645 _config_template = """
646 newServer{address="127.0.0.1:%s", useClientSubnet=true}
647 addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" })
648 setECSOverride(true)
649 """
650 _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey']
651
652 def testDOHSimple(self):
653 """
654 DOH with ECS: Simple query
655 """
656 name = 'simple.doh-ecs.tests.powerdns.com.'
657 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
658 query.id = 0
659 rewrittenEcso = clientsubnetoption.ClientSubnetOption('127.0.0.0', 24)
660 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096, options=[rewrittenEcso])
661 response = dns.message.make_response(query)
662 rrset = dns.rrset.from_text(name,
663 3600,
664 dns.rdataclass.IN,
665 dns.rdatatype.A,
666 '127.0.0.1')
667 response.answer.append(rrset)
668
669 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert)
670 self.assertTrue(receivedQuery)
671 self.assertTrue(receivedResponse)
672 expectedQuery.id = receivedQuery.id
673 self.assertEquals(expectedQuery, receivedQuery)
674 self.checkQueryEDNSWithECS(expectedQuery, receivedQuery)
675 self.assertEquals(response, receivedResponse)
676 self.checkResponseNoEDNS(response, receivedResponse)
677
678 def testDOHExistingEDNS(self):
679 """
680 DOH with ECS: Existing EDNS
681 """
682 name = 'existing-edns.doh-ecs.tests.powerdns.com.'
683 query = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=8192)
684 query.id = 0
685 rewrittenEcso = clientsubnetoption.ClientSubnetOption('127.0.0.0', 24)
686 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=8192, options=[rewrittenEcso])
687 response = dns.message.make_response(query)
688 rrset = dns.rrset.from_text(name,
689 3600,
690 dns.rdataclass.IN,
691 dns.rdatatype.A,
692 '127.0.0.1')
693 response.answer.append(rrset)
694
695 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert)
696 self.assertTrue(receivedQuery)
697 self.assertTrue(receivedResponse)
698 receivedQuery.id = expectedQuery.id
699 self.assertEquals(expectedQuery, receivedQuery)
700 self.assertEquals(response, receivedResponse)
701 self.checkQueryEDNSWithECS(expectedQuery, receivedQuery)
702 self.checkResponseEDNSWithoutECS(response, receivedResponse)
703
704 def testDOHExistingECS(self):
705 """
706 DOH with ECS: Existing EDNS Client Subnet
707 """
708 name = 'existing-ecs.doh-ecs.tests.powerdns.com.'
709 ecso = clientsubnetoption.ClientSubnetOption('1.2.3.4')
710 rewrittenEcso = clientsubnetoption.ClientSubnetOption('127.0.0.0', 24)
711 query = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=512, options=[ecso], want_dnssec=True)
712 query.id = 0
713 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=512, options=[rewrittenEcso])
714 response = dns.message.make_response(query)
715 response.use_edns(edns=True, payload=4096, options=[rewrittenEcso])
716 rrset = dns.rrset.from_text(name,
717 3600,
718 dns.rdataclass.IN,
719 dns.rdatatype.A,
720 '127.0.0.1')
721 response.answer.append(rrset)
722
723 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert)
724 self.assertTrue(receivedQuery)
725 self.assertTrue(receivedResponse)
726 receivedQuery.id = expectedQuery.id
727 self.assertEquals(expectedQuery, receivedQuery)
728 self.assertEquals(response, receivedResponse)
729 self.checkQueryEDNSWithECS(expectedQuery, receivedQuery)
730 self.checkResponseEDNSWithECS(response, receivedResponse)
731
732 class TestDOHOverHTTP(DNSDistDOHTest):
733
734 _dohServerPort = 8480
735 _serverName = 'tls.tests.dnsdist.org'
736 _dohBaseURL = ("http://%s:%d/" % (_serverName, _dohServerPort))
737 _config_template = """
738 newServer{address="127.0.0.1:%s"}
739 addDOHLocal("127.0.0.1:%s")
740 """
741 _config_params = ['_testServerPort', '_dohServerPort']
742 _checkConfigExpectedOutput = b"""No certificate provided for DoH endpoint 127.0.0.1:8480, running in DNS over HTTP mode instead of DNS over HTTPS
743 Configuration 'configs/dnsdist_TestDOHOverHTTP.conf' OK!
744 """
745
746 def testDOHSimple(self):
747 """
748 DOH over HTTP: Simple query
749 """
750 name = 'simple.doh-over-http.tests.powerdns.com.'
751 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
752 query.id = 0
753 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
754 response = dns.message.make_response(query)
755 rrset = dns.rrset.from_text(name,
756 3600,
757 dns.rdataclass.IN,
758 dns.rdatatype.A,
759 '127.0.0.1')
760 response.answer.append(rrset)
761
762 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, useHTTPS=False)
763 self.assertTrue(receivedQuery)
764 self.assertTrue(receivedResponse)
765 expectedQuery.id = receivedQuery.id
766 self.assertEquals(expectedQuery, receivedQuery)
767 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
768 self.assertEquals(response, receivedResponse)
769 self.checkResponseNoEDNS(response, receivedResponse)
770
771 def testDOHSimplePOST(self):
772 """
773 DOH over HTTP: Simple POST query
774 """
775 name = 'simple-post.doh-over-http.tests.powerdns.com.'
776 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
777 query.id = 0
778 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
779 expectedQuery.id = 0
780 response = dns.message.make_response(query)
781 rrset = dns.rrset.from_text(name,
782 3600,
783 dns.rdataclass.IN,
784 dns.rdatatype.A,
785 '127.0.0.1')
786 response.answer.append(rrset)
787
788 (receivedQuery, receivedResponse) = self.sendDOHPostQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, useHTTPS=False)
789 self.assertTrue(receivedQuery)
790 self.assertTrue(receivedResponse)
791 receivedQuery.id = expectedQuery.id
792 self.assertEquals(expectedQuery, receivedQuery)
793 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
794 self.assertEquals(response, receivedResponse)
795 self.checkResponseNoEDNS(response, receivedResponse)
796
797 class TestDOHWithCache(DNSDistDOHTest):
798
799 _serverKey = 'server.key'
800 _serverCert = 'server.chain'
801 _serverName = 'tls.tests.dnsdist.org'
802 _caCert = 'ca.pem'
803 _dohServerPort = 8443
804 _dohBaseURL = ("https://%s:%d/" % (_serverName, _dohServerPort))
805 _config_template = """
806 newServer{address="127.0.0.1:%s"}
807
808 addDOHLocal("127.0.0.1:%s", "%s", "%s")
809
810 pc = newPacketCache(100, {maxTTL=86400, minTTL=1})
811 getPool(""):setCache(pc)
812 """
813 _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey']
814
815 def testDOHCacheLargeAnswer(self):
816 """
817 DOH with cache: Check that we can cache (and retrieve) large answers
818 """
819 numberOfQueries = 10
820 name = 'large.doh-with-cache.tests.powerdns.com.'
821 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
822 query.id = 0
823 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
824 expectedQuery.id = 0
825 response = dns.message.make_response(query)
826 # we prepare a large answer
827 content = ""
828 for i in range(44):
829 if len(content) > 0:
830 content = content + ', '
831 content = content + (str(i)*50)
832 # pad up to 4096
833 content = content + 'A'*40
834
835 rrset = dns.rrset.from_text(name,
836 3600,
837 dns.rdataclass.IN,
838 dns.rdatatype.TXT,
839 content)
840 response.answer.append(rrset)
841 self.assertEquals(len(response.to_wire()), 4096)
842
843 # first query to fill the cache
844 (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert)
845 self.assertTrue(receivedQuery)
846 self.assertTrue(receivedResponse)
847 receivedQuery.id = expectedQuery.id
848 self.assertEquals(expectedQuery, receivedQuery)
849 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
850 self.assertEquals(response, receivedResponse)
851
852 for _ in range(numberOfQueries):
853 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, caFile=self._caCert, useQueue=False)
854 self.assertEquals(receivedResponse, response)