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