]> git.ipfire.org Git - thirdparty/pdns.git/blob - regression-tests.dnsdist/test_DOH.py
dnsdist: Add more FFI regression test (DoH, EDNS options and tags)
[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 re
6 import time
7 import unittest
8 import clientsubnetoption
9 from dnsdisttests import DNSDistTest
10
11 import pycurl
12 from io import BytesIO
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 def getHeaderValue(self, name):
114 for header in self._response_headers.decode().splitlines(False):
115 values = header.split(':')
116 key = values[0]
117 if key.lower() == name.lower():
118 return values[1].strip()
119 return None
120
121 def checkHasHeader(self, name, value):
122 got = self.getHeaderValue(name)
123 self.assertEquals(got, value)
124
125 def checkNoHeader(self, name):
126 self.checkHasHeader(name, None)
127
128 @classmethod
129 def setUpClass(cls):
130
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')
134
135 cls.startResponders()
136 cls.startDNSDist()
137 cls.setUpSockets()
138
139 print("Launching tests..")
140
141 class TestDOH(DNSDistDOHTest):
142
143 _serverKey = 'server.key'
144 _serverCert = 'server.chain'
145 _serverName = 'tls.tests.dnsdist.org'
146 _caCert = 'ca.pem'
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"}
153
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'})})
157
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"))
166
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
172 foundct = true
173 break
174 end
175 end
176 if foundct then
177 dq:setHTTPResponse(200, 'It works!', 'text/plain')
178 dq.dh:setQR(true)
179 return DNSAction.HeaderModify
180 end
181 end
182 return DNSAction.None
183 end
184 addAction("http-lua.doh.tests.powerdns.com.", LuaAction(dohHandler))
185 """
186 _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey', '_serverName', '_dohServerPort']
187
188 def testDOHSimple(self):
189 """
190 DOH: Simple query
191 """
192 name = 'simple.doh.tests.powerdns.com.'
193 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
194 query.id = 0
195 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
196 expectedQuery.id = 0
197 response = dns.message.make_response(query)
198 rrset = dns.rrset.from_text(name,
199 3600,
200 dns.rdataclass.IN,
201 dns.rdatatype.A,
202 '127.0.0.1')
203 response.answer.append(rrset)
204
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._customResponseHeader1) in self._response_headers.decode())
211 self.assertTrue((self._customResponseHeader2) 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')
218
219 def testDOHSimplePOST(self):
220 """
221 DOH: Simple POST query
222 """
223 name = 'simple-post.doh.tests.powerdns.com.'
224 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
225 query.id = 0
226 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
227 expectedQuery.id = 0
228 response = dns.message.make_response(query)
229 rrset = dns.rrset.from_text(name,
230 3600,
231 dns.rdataclass.IN,
232 dns.rdatatype.A,
233 '127.0.0.1')
234 response.answer.append(rrset)
235
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)
243
244 def testDOHExistingEDNS(self):
245 """
246 DOH: Existing EDNS
247 """
248 name = 'existing-edns.doh.tests.powerdns.com.'
249 query = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=8192)
250 query.id = 0
251 response = dns.message.make_response(query)
252 rrset = dns.rrset.from_text(name,
253 3600,
254 dns.rdataclass.IN,
255 dns.rdatatype.A,
256 '127.0.0.1')
257 response.answer.append(rrset)
258
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)
267
268 def testDOHExistingECS(self):
269 """
270 DOH: Existing EDNS Client Subnet
271 """
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)
276 query.id = 0
277 response = dns.message.make_response(query)
278 response.use_edns(edns=True, payload=4096, options=[rewrittenEcso])
279 rrset = dns.rrset.from_text(name,
280 3600,
281 dns.rdataclass.IN,
282 dns.rdatatype.A,
283 '127.0.0.1')
284 response.answer.append(rrset)
285
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)
294
295 def testDropped(self):
296 """
297 DOH: Dropped query
298 """
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)
303
304 def testRefused(self):
305 """
306 DOH: Refused
307 """
308 name = 'refused.doh.tests.powerdns.com.'
309 query = dns.message.make_query(name, 'A', 'IN')
310 query.id = 0
311 query.flags &= ~dns.flags.RD
312 expectedResponse = dns.message.make_response(query)
313 expectedResponse.set_rcode(dns.rcode.REFUSED)
314
315 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, caFile=self._caCert, query=query, response=None, useQueue=False)
316 self.assertEquals(receivedResponse, expectedResponse)
317
318 def testSpoof(self):
319 """
320 DOH: Spoofed
321 """
322 name = 'spoof.doh.tests.powerdns.com.'
323 query = dns.message.make_query(name, 'A', 'IN')
324 query.id = 0
325 query.flags &= ~dns.flags.RD
326 expectedResponse = dns.message.make_response(query)
327 rrset = dns.rrset.from_text(name,
328 3600,
329 dns.rdataclass.IN,
330 dns.rdatatype.A,
331 '1.2.3.4')
332 expectedResponse.answer.append(rrset)
333
334 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, caFile=self._caCert, query=query, response=None, useQueue=False)
335 self.assertEquals(receivedResponse, expectedResponse)
336
337 def testDOHInvalid(self):
338 """
339 DOH: Invalid query
340 """
341 name = 'invalid.doh.tests.powerdns.com.'
342 invalidQuery = dns.message.make_query(name, 'A', 'IN', use_edns=False)
343 invalidQuery.id = 0
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)
349
350 # and now a valid one
351 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
352 query.id = 0
353 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
354 expectedQuery.id = 0
355 response = dns.message.make_response(query)
356 rrset = dns.rrset.from_text(name,
357 3600,
358 dns.rdataclass.IN,
359 dns.rdatatype.A,
360 '127.0.0.1')
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)
369
370 def testDOHWithoutQuery(self):
371 """
372 DOH: Empty GET query
373 """
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)
385
386 def testDOHEmptyPOST(self):
387 """
388 DOH: Empty POST query
389 """
390 name = 'empty-post.doh.tests.powerdns.com.'
391
392 (_, receivedResponse) = self.sendDOHPostQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query="", rawQuery=True, response=None, caFile=self._caCert)
393 self.assertEquals(receivedResponse, None)
394
395 # and now a valid one
396 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
397 query.id = 0
398 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
399 expectedQuery.id = 0
400 response = dns.message.make_response(query)
401 rrset = dns.rrset.from_text(name,
402 3600,
403 dns.rdataclass.IN,
404 dns.rdatatype.A,
405 '127.0.0.1')
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)
414
415 def testHeaderRule(self):
416 """
417 DOH: HeaderRule
418 """
419 name = 'header-rule.doh.tests.powerdns.com.'
420 query = dns.message.make_query(name, 'A', 'IN')
421 query.id = 0
422 query.flags &= ~dns.flags.RD
423 expectedResponse = dns.message.make_response(query)
424 rrset = dns.rrset.from_text(name,
425 3600,
426 dns.rdataclass.IN,
427 dns.rdatatype.A,
428 '2.3.4.5')
429 expectedResponse.answer.append(rrset)
430
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)
434
435 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
436 expectedQuery.flags &= ~dns.flags.RD
437 expectedQuery.id = 0
438 response = dns.message.make_response(query)
439 rrset = dns.rrset.from_text(name,
440 3600,
441 dns.rdataclass.IN,
442 dns.rdatatype.A,
443 '127.0.0.1')
444 response.answer.append(rrset)
445
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)
454
455 def testHTTPPath(self):
456 """
457 DOH: HTTPPath
458 """
459 name = 'http-path.doh.tests.powerdns.com.'
460 query = dns.message.make_query(name, 'A', 'IN')
461 query.id = 0
462 query.flags &= ~dns.flags.RD
463 expectedResponse = dns.message.make_response(query)
464 rrset = dns.rrset.from_text(name,
465 3600,
466 dns.rdataclass.IN,
467 dns.rdatatype.A,
468 '3.4.5.6')
469 expectedResponse.answer.append(rrset)
470
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)
474
475 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
476 expectedQuery.id = 0
477 expectedQuery.flags &= ~dns.flags.RD
478 response = dns.message.make_response(query)
479 rrset = dns.rrset.from_text(name,
480 3600,
481 dns.rdataclass.IN,
482 dns.rdatatype.A,
483 '127.0.0.1')
484 response.answer.append(rrset)
485
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)
494
495 def testHTTPPathRegex(self):
496 """
497 DOH: HTTPPathRegex
498 """
499 name = 'http-path-regex.doh.tests.powerdns.com.'
500 query = dns.message.make_query(name, 'A', 'IN')
501 query.id = 0
502 query.flags &= ~dns.flags.RD
503 expectedResponse = dns.message.make_response(query)
504 rrset = dns.rrset.from_text(name,
505 3600,
506 dns.rdataclass.IN,
507 dns.rdatatype.A,
508 '6.7.8.9')
509 expectedResponse.answer.append(rrset)
510
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)
514
515 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
516 expectedQuery.id = 0
517 expectedQuery.flags &= ~dns.flags.RD
518 response = dns.message.make_response(query)
519 rrset = dns.rrset.from_text(name,
520 3600,
521 dns.rdataclass.IN,
522 dns.rdatatype.A,
523 '127.0.0.1')
524 response.answer.append(rrset)
525
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)
534
535 def testHTTPStatusAction200(self):
536 """
537 DOH: HTTPStatusAction 200 OK
538 """
539 name = 'http-status-action.doh.tests.powerdns.com.'
540 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
541 query.id = 0
542
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())
548
549 def testHTTPStatusAction307(self):
550 """
551 DOH: HTTPStatusAction 307
552 """
553 name = 'http-status-action-redirect.doh.tests.powerdns.com.'
554 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
555 query.id = 0
556
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())
561
562 def testHTTPLuaResponse(self):
563 """
564 DOH: Lua HTTP Response
565 """
566 name = 'http-lua.doh.tests.powerdns.com.'
567 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
568 query.id = 0
569
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())
575
576 def testHTTPEarlyResponse(self):
577 """
578 DOH: HTTP Early Response
579 """
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()
592
593 self.assertEquals(rcode, 418)
594 self.assertEquals(data, b'C0FFEE')
595 self.assertIn('foo: bar', headers)
596 self.assertNotIn(self._customResponseHeader2, headers)
597
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)
607 data = ''
608 conn.setopt(pycurl.POSTFIELDS, data)
609
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._customResponseHeader2, headers)
617
618 class TestDOHAddingECS(DNSDistDOHTest):
619
620 _serverKey = 'server.key'
621 _serverCert = 'server.chain'
622 _serverName = 'tls.tests.dnsdist.org'
623 _caCert = 'ca.pem'
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", { "/" })
629 setECSOverride(true)
630 """
631 _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey']
632
633 def testDOHSimple(self):
634 """
635 DOH with ECS: Simple query
636 """
637 name = 'simple.doh-ecs.tests.powerdns.com.'
638 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
639 query.id = 0
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,
644 3600,
645 dns.rdataclass.IN,
646 dns.rdatatype.A,
647 '127.0.0.1')
648 response.answer.append(rrset)
649
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)
658
659 def testDOHExistingEDNS(self):
660 """
661 DOH with ECS: Existing EDNS
662 """
663 name = 'existing-edns.doh-ecs.tests.powerdns.com.'
664 query = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=8192)
665 query.id = 0
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,
670 3600,
671 dns.rdataclass.IN,
672 dns.rdatatype.A,
673 '127.0.0.1')
674 response.answer.append(rrset)
675
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)
684
685 def testDOHExistingECS(self):
686 """
687 DOH with ECS: Existing EDNS Client Subnet
688 """
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)
693 query.id = 0
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,
698 3600,
699 dns.rdataclass.IN,
700 dns.rdatatype.A,
701 '127.0.0.1')
702 response.answer.append(rrset)
703
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)
712
713 class TestDOHOverHTTP(DNSDistDOHTest):
714
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")
721 """
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!
725 """
726
727 def testDOHSimple(self):
728 """
729 DOH over HTTP: Simple query
730 """
731 name = 'simple.doh-over-http.tests.powerdns.com.'
732 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
733 query.id = 0
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,
737 3600,
738 dns.rdataclass.IN,
739 dns.rdatatype.A,
740 '127.0.0.1')
741 response.answer.append(rrset)
742
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)
751
752 def testDOHSimplePOST(self):
753 """
754 DOH over HTTP: Simple POST query
755 """
756 name = 'simple-post.doh-over-http.tests.powerdns.com.'
757 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
758 query.id = 0
759 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
760 expectedQuery.id = 0
761 response = dns.message.make_response(query)
762 rrset = dns.rrset.from_text(name,
763 3600,
764 dns.rdataclass.IN,
765 dns.rdatatype.A,
766 '127.0.0.1')
767 response.answer.append(rrset)
768
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)
777
778 class TestDOHWithCache(DNSDistDOHTest):
779
780 _serverKey = 'server.key'
781 _serverCert = 'server.chain'
782 _serverName = 'tls.tests.dnsdist.org'
783 _caCert = 'ca.pem'
784 _dohServerPort = 8443
785 _dohBaseURL = ("https://%s:%d/" % (_serverName, _dohServerPort))
786 _config_template = """
787 newServer{address="127.0.0.1:%s"}
788
789 addDOHLocal("127.0.0.1:%s", "%s", "%s")
790
791 pc = newPacketCache(100, {maxTTL=86400, minTTL=1})
792 getPool(""):setCache(pc)
793 """
794 _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey']
795
796 def testDOHCacheLargeAnswer(self):
797 """
798 DOH with cache: Check that we can cache (and retrieve) large answers
799 """
800 numberOfQueries = 10
801 name = 'large.doh-with-cache.tests.powerdns.com.'
802 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
803 query.id = 0
804 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
805 expectedQuery.id = 0
806 response = dns.message.make_response(query)
807 # we prepare a large answer
808 content = ""
809 for i in range(44):
810 if len(content) > 0:
811 content = content + ', '
812 content = content + (str(i)*50)
813 # pad up to 4096
814 content = content + 'A'*40
815
816 rrset = dns.rrset.from_text(name,
817 3600,
818 dns.rdataclass.IN,
819 dns.rdatatype.TXT,
820 content)
821 response.answer.append(rrset)
822 self.assertEquals(len(response.to_wire()), 4096)
823
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')
833
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))
838
839 time.sleep(1)
840
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))
844
845 class TestDOHWithoutCacheControl(DNSDistDOHTest):
846
847 _serverKey = 'server.key'
848 _serverCert = 'server.chain'
849 _serverName = 'tls.tests.dnsdist.org'
850 _caCert = 'ca.pem'
851 _dohServerPort = 8443
852 _dohBaseURL = ("https://%s:%d/" % (_serverName, _dohServerPort))
853 _config_template = """
854 newServer{address="127.0.0.1:%s"}
855
856 addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" }, {sendCacheControlHeaders=false})
857 """
858 _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey']
859
860 def testDOHSimple(self):
861 """
862 DOH without cache-control
863 """
864 name = 'simple.doh.tests.powerdns.com.'
865 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
866 query.id = 0
867 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
868 expectedQuery.id = 0
869 response = dns.message.make_response(query)
870 rrset = dns.rrset.from_text(name,
871 3600,
872 dns.rdataclass.IN,
873 dns.rdatatype.A,
874 '127.0.0.1')
875 response.answer.append(rrset)
876
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)
885
886 class TestDOHFFI(DNSDistDOHTest):
887
888 _serverKey = 'server.key'
889 _serverCert = 'server.chain'
890 _serverName = 'tls.tests.dnsdist.org'
891 _caCert = 'ca.pem'
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"}
898
899 addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" }, {customResponseHeaders={["access-control-allow-origin"]="*",["user-agent"]="derp",["UPPERCASE"]="VaLuE"}})
900
901 local ffi = require("ffi")
902
903 ffi.cdef[[
904 typedef struct dnsdist_ffi_dnsquestion_t dnsdist_ffi_dnsquestion_t;
905
906 typedef struct dnsdist_http_header {
907 const char* name;
908 const char* value;
909 } dnsdist_http_header_t;
910
911 void dnsdist_ffi_dnsquestion_get_sni(const dnsdist_ffi_dnsquestion_t* dq, const char** sni, size_t* sniSize) __attribute__ ((visibility ("default")));
912
913 const char* dnsdist_ffi_dnsquestion_get_http_path(dnsdist_ffi_dnsquestion_t* dq) __attribute__ ((visibility ("default")));
914 const char* dnsdist_ffi_dnsquestion_get_http_query_string(dnsdist_ffi_dnsquestion_t* dq) __attribute__ ((visibility ("default")));
915 const char* dnsdist_ffi_dnsquestion_get_http_host(dnsdist_ffi_dnsquestion_t* dq) __attribute__ ((visibility ("default")));
916 const char* dnsdist_ffi_dnsquestion_get_http_scheme(dnsdist_ffi_dnsquestion_t* dq) __attribute__ ((visibility ("default")));
917
918 size_t dnsdist_ffi_dnsquestion_get_http_headers(dnsdist_ffi_dnsquestion_t* ref, const dnsdist_http_header_t** out) __attribute__ ((visibility ("default")));
919
920 void dnsdist_ffi_dnsquestion_set_http_response(dnsdist_ffi_dnsquestion_t* dq, uint16_t statusCode, const char* body, const char* contentType) __attribute__ ((visibility ("default")));
921 ]]
922
923 function dohHandler(dq)
924 local scheme = ffi.string(ffi.C.dnsdist_ffi_dnsquestion_get_http_scheme(dq))
925 local host = ffi.string(ffi.C.dnsdist_ffi_dnsquestion_get_http_host(dq))
926 local path = ffi.string(ffi.C.dnsdist_ffi_dnsquestion_get_http_path(dq))
927 local query_string = ffi.string(ffi.C.dnsdist_ffi_dnsquestion_get_http_query_string(dq))
928 if scheme == 'https' and host == '%s:%d' and path == '/' and query_string == '' then
929 local foundct = false
930 local headers_ptr = ffi.new("const dnsdist_http_header_t *[1]")
931 local headers_ptr_param = ffi.cast("const dnsdist_http_header_t **", headers_ptr)
932
933 local headers_count = tonumber(ffi.C.dnsdist_ffi_dnsquestion_get_http_headers(dq, headers_ptr_param))
934 if headers_count > 0 then
935 for idx = 0, headers_count-1 do
936 if ffi.string(headers_ptr[0][idx].name) == 'content-type' and ffi.string(headers_ptr[0][idx].value) == 'application/dns-message' then
937 foundct = true
938 break
939 end
940 end
941 end
942 if foundct then
943 ffi.C.dnsdist_ffi_dnsquestion_set_http_response(dq, 200, 'It works!', 'text/plain')
944 return DNSAction.HeaderModify
945 end
946 end
947 return DNSAction.None
948 end
949 addAction("http-lua-ffi.doh.tests.powerdns.com.", LuaFFIAction(dohHandler))
950 """
951 _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey', '_serverName', '_dohServerPort']
952
953 def testHTTPLuaFFIResponse(self):
954 """
955 DOH: Lua FFI HTTP Response
956 """
957 name = 'http-lua-ffi.doh.tests.powerdns.com.'
958 query = dns.message.make_query(name, 'A', 'IN', use_edns=False)
959 query.id = 0
960
961 (_, receivedResponse) = self.sendDOHPostQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, caFile=self._caCert, useQueue=False, rawResponse=True)
962 self.assertTrue(receivedResponse)
963 self.assertEquals(receivedResponse, b'It works!')
964 self.assertEquals(self._rcode, 200)
965 self.assertTrue('content-type: text/plain' in self._response_headers.decode())
966