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