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