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