]> git.ipfire.org Git - thirdparty/pdns.git/blame - regression-tests.dnsdist/test_DOH.py
Merge pull request #13861 from omoerbeek/rec-rpzloader-tidy
[thirdparty/pdns.git] / regression-tests.dnsdist / test_DOH.py
CommitLineData
10535d88 1#!/usr/bin/env python
fda32c1c 2
c02b7e13 3import base64
10535d88 4import dns
13291274 5import os
0026abf9 6import time
13291274 7import unittest
10535d88 8import clientsubnetoption
1c9c001c
RG
9
10from dnsdistdohtests import DNSDistDOHTest
630eb526 11from dnsdisttests import pickAvailablePort
10535d88
RG
12
13import pycurl
811872fb 14from io import BytesIO
10535d88 15
d2c3ef4b 16class DOHTests(object):
10535d88
RG
17 _serverKey = 'server.key'
18 _serverCert = 'server.chain'
19 _serverName = 'tls.tests.dnsdist.org'
20 _caCert = 'ca.pem'
630eb526 21 _dohServerPort = pickAvailablePort()
ee01507f
CR
22 _customResponseHeader1 = 'access-control-allow-origin: *'
23 _customResponseHeader2 = 'user-agent: derp'
10535d88
RG
24 _dohBaseURL = ("https://%s:%d/" % (_serverName, _dohServerPort))
25 _config_template = """
d2c3ef4b 26 newServer{address="127.0.0.1:%d"}
10535d88
RG
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"))
9ba32868
RG
31 addAction(HTTPHeaderRule("X-PowerDNS", "^[a]{5}$"), SpoofAction("2.3.4.5"))
32 addAction(HTTPPathRule("/PowerDNS"), SpoofAction("3.4.5.6"))
9676d2a9
RG
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"))
c02b7e13 36 addAction("no-backend.doh.tests.powerdns.com.", PoolAction('this-pool-has-no-backend'))
9676d2a9
RG
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))
d2c3ef4b
RG
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'})})
10535d88 60 """
d2c3ef4b
RG
61 _config_params = ['_testServerPort', '_serverName', '_dohServerPort', '_dohServerPort', '_serverCert', '_serverKey', '_dohLibrary']
62 _verboseMode = True
10535d88
RG
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
4bfebc93 85 self.assertEqual(expectedQuery, receivedQuery)
811872fb
RG
86 self.assertTrue((self._customResponseHeader1) in self._response_headers.decode())
87 self.assertTrue((self._customResponseHeader2) in self._response_headers.decode())
cf3e149b
RG
88 self.assertFalse(('UPPERCASE: VaLuE' in self._response_headers.decode()))
89 self.assertTrue(('uppercase: VaLuE' in self._response_headers.decode()))
0026abf9 90 self.assertTrue(('cache-control: max-age=3600' in self._response_headers.decode()))
10535d88 91 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
4bfebc93 92 self.assertEqual(response, receivedResponse)
0026abf9 93 self.checkHasHeader('cache-control', 'max-age=3600')
10535d88 94
80a63659
RG
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
4bfebc93 116 self.assertEqual(expectedQuery, receivedQuery)
80a63659 117 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
4bfebc93 118 self.assertEqual(response, receivedResponse)
80a63659 119 # just to be sure the ID _is_ checked
4bfebc93 120 self.assertEqual(response.id, receivedResponse.id)
80a63659 121
b1e527ad
RG
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
4bfebc93 143 self.assertEqual(expectedQuery, receivedQuery)
b1e527ad 144 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
4bfebc93 145 self.assertEqual(response, receivedResponse)
b1e527ad 146
10535d88
RG
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
4bfebc93
CH
166 self.assertEqual(query, receivedQuery)
167 self.assertEqual(response, receivedResponse)
10535d88
RG
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
4bfebc93
CH
193 self.assertEqual(query, receivedQuery)
194 self.assertEqual(response, receivedResponse)
10535d88
RG
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)
4bfebc93 205 self.assertEqual(receivedResponse, None)
10535d88
RG
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
7af22479 214 query.flags &= ~dns.flags.RD
10535d88
RG
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)
4bfebc93 219 self.assertEqual(receivedResponse, expectedResponse)
10535d88
RG
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)
4bfebc93 238 self.assertEqual(receivedResponse, expectedResponse)
10535d88 239
c02b7e13
RG
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
a56b8c0f
RG
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
c02b7e13
RG
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
7f84fd45
RG
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)
a79980a3 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')
7f84fd45 404
2a3c2b44
RG
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
47225117
RG
417 def testDOHInvalid(self):
418 """
c02b7e13 419 DOH: Invalid DNS query
47225117
RG
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)
4bfebc93 428 self.assertEqual(receivedResponse, None)
47225117
RG
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
4bfebc93 446 self.assertEqual(expectedQuery, receivedQuery)
47225117 447 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
4bfebc93 448 self.assertEqual(response, receivedResponse)
10535d88 449
c02b7e13 450 def testDOHInvalidHeaderName(self):
2cb8efb1 451 """
c02b7e13 452 DOH: Invalid HTTP header name query
2cb8efb1 453 """
c02b7e13
RG
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)
2cb8efb1
RG
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)
c02b7e13 494 self.assertEqual(rcode, 403)
2cb8efb1 495
b1e527ad
RG
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)
4bfebc93 503 self.assertEqual(receivedResponse, None)
b1e527ad
RG
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
4bfebc93 521 self.assertEqual(expectedQuery, receivedQuery)
b1e527ad 522 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
4bfebc93 523 self.assertEqual(response, receivedResponse)
b1e527ad 524
9ba32868
RG
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'])
4bfebc93 543 self.assertEqual(receivedResponse, expectedResponse)
9ba32868
RG
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
4bfebc93 561 self.assertEqual(expectedQuery, receivedQuery)
9ba32868 562 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
4bfebc93 563 self.assertEqual(response, receivedResponse)
9ba32868
RG
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)
4bfebc93 583 self.assertEqual(receivedResponse, expectedResponse)
9ba32868
RG
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
4bfebc93 601 self.assertEqual(expectedQuery, receivedQuery)
9ba32868 602 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
4bfebc93 603 self.assertEqual(response, receivedResponse)
9ba32868 604
767fbba3
RG
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)
4bfebc93
CH
608 self.assertEqual(receivedResponse, b'there is no endpoint configured for this path')
609 self.assertEqual(self._rcode, 404)
767fbba3 610
9676d2a9
RG
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)
4bfebc93 629 self.assertEqual(receivedResponse, expectedResponse)
9676d2a9
RG
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
4bfebc93 647 self.assertEqual(expectedQuery, receivedQuery)
9676d2a9 648 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
4bfebc93 649 self.assertEqual(response, receivedResponse)
9676d2a9
RG
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)
4bfebc93
CH
661 self.assertEqual(receivedResponse, b'Plaintext answer')
662 self.assertEqual(self._rcode, 200)
9676d2a9
RG
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)
4bfebc93 675 self.assertEqual(self._rcode, 307)
9676d2a9
RG
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)
4bfebc93
CH
688 self.assertEqual(receivedResponse, b'It works!')
689 self.assertEqual(self._rcode, 200)
9676d2a9
RG
690 self.assertTrue('content-type: text/plain' in self._response_headers.decode())
691
28b56482
RG
692 def testHTTPEarlyResponse(self):
693 """
694 DOH: HTTP Early Response
695 """
ded6907c 696 response_headers = BytesIO()
28b56482
RG
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)
ded6907c 704 conn.setopt(pycurl.HEADERFUNCTION, response_headers.write)
28b56482
RG
705 data = conn.perform_rb()
706 rcode = conn.getinfo(pycurl.RESPONSE_CODE)
9b2ef603 707 headers = response_headers.getvalue().decode()
28b56482 708
4bfebc93
CH
709 self.assertEqual(rcode, 418)
710 self.assertEqual(data, b'C0FFEE')
ded6907c
RG
711 self.assertIn('foo: bar', headers)
712 self.assertNotIn(self._customResponseHeader2, headers)
28b56482 713
ded6907c 714 response_headers = BytesIO()
28b56482
RG
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)
ded6907c 721 conn.setopt(pycurl.HEADERFUNCTION, response_headers.write)
28b56482
RG
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)
9b2ef603 728 headers = response_headers.getvalue().decode()
4bfebc93
CH
729 self.assertEqual(rcode, 418)
730 self.assertEqual(data, b'C0FFEE')
ded6907c
RG
731 self.assertIn('foo: bar', headers)
732 self.assertNotIn(self._customResponseHeader2, headers)
28b56482 733
d2c3ef4b
RG
734class TestDoHNGHTTP2(DOHTests, DNSDistDOHTest):
735 _dohLibrary = 'nghttp2'
14104a18 736
d2c3ef4b
RG
737class TestDoHH2O(DOHTests, DNSDistDOHTest):
738 _dohLibrary = 'h2o'
739
740class DOHSubPathsTests(object):
14104a18
RG
741 _serverKey = 'server.key'
742 _serverCert = 'server.chain'
743 _serverName = 'tls.tests.dnsdist.org'
744 _caCert = 'ca.pem'
630eb526 745 _dohServerPort = pickAvailablePort()
14104a18
RG
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
d2c3ef4b 752 addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/PowerDNS" }, {exactPathMatching=false, library='%s'})
14104a18 753 """
d2c3ef4b 754 _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey', '_dohLibrary']
14104a18
RG
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)
4bfebc93 774 self.assertEqual(receivedResponse, expectedResponse)
14104a18
RG
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)
7e8a05fa 790 self.assertIn(receivedResponse, [b'there is no endpoint configured for this path', b'not found'])
4bfebc93 791 self.assertEqual(self._rcode, 404)
14104a18
RG
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)
4bfebc93 795 self.assertEqual(receivedResponse, expectedResponse)
14104a18 796
d2c3ef4b
RG
797class TestDoHSubPathsNGHTTP2(DOHSubPathsTests, DNSDistDOHTest):
798 _dohLibrary = 'nghttp2'
799
800class TestDoHSubPathsH2O(DOHSubPathsTests, DNSDistDOHTest):
801 _dohLibrary = 'h2o'
802
803class DOHAddingECSTests(object):
10535d88
RG
804
805 _serverKey = 'server.key'
806 _serverCert = 'server.chain'
807 _serverName = 'tls.tests.dnsdist.org'
808 _caCert = 'ca.pem'
630eb526 809 _dohServerPort = pickAvailablePort()
10535d88
RG
810 _dohBaseURL = ("https://%s:%d/" % (_serverName, _dohServerPort))
811 _config_template = """
812 newServer{address="127.0.0.1:%s", useClientSubnet=true}
d2c3ef4b 813 addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" }, {library='%s'})
10535d88
RG
814 setECSOverride(true)
815 """
d2c3ef4b 816 _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey', '_dohLibrary']
10535d88
RG
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
4bfebc93 839 self.assertEqual(expectedQuery, receivedQuery)
10535d88 840 self.checkQueryEDNSWithECS(expectedQuery, receivedQuery)
4bfebc93 841 self.assertEqual(response, receivedResponse)
10535d88
RG
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
4bfebc93
CH
865 self.assertEqual(expectedQuery, receivedQuery)
866 self.assertEqual(response, receivedResponse)
10535d88
RG
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
4bfebc93
CH
893 self.assertEqual(expectedQuery, receivedQuery)
894 self.assertEqual(response, receivedResponse)
10535d88
RG
895 self.checkQueryEDNSWithECS(expectedQuery, receivedQuery)
896 self.checkResponseEDNSWithECS(response, receivedResponse)
44947230 897
d2c3ef4b
RG
898class TestDoHAddingECSNGHTTP2(DOHAddingECSTests, DNSDistDOHTest):
899 _dohLibrary = 'nghttp2'
900
901class TestDoHAddingECSH2O(DOHAddingECSTests, DNSDistDOHTest):
902 _dohLibrary = 'h2o'
44947230 903
d2c3ef4b 904class DOHOverHTTP(object):
630eb526 905 _dohServerPort = pickAvailablePort()
44947230 906 _serverName = 'tls.tests.dnsdist.org'
a6f94324 907 _dohBaseURL = ("http://%s:%d/dns-query" % (_serverName, _dohServerPort))
44947230
RG
908 _config_template = """
909 newServer{address="127.0.0.1:%s"}
d2c3ef4b 910 addDOHLocal("127.0.0.1:%s", nil, nil, '/dns-query', {library='%s'})
44947230 911 """
d2c3ef4b 912 _config_params = ['_testServerPort', '_dohServerPort', '_dohLibrary']
44947230
RG
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
4bfebc93 934 self.assertEqual(expectedQuery, receivedQuery)
44947230 935 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
4bfebc93 936 self.assertEqual(response, receivedResponse)
44947230
RG
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
4bfebc93 960 self.assertEqual(expectedQuery, receivedQuery)
44947230 961 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
4bfebc93 962 self.assertEqual(response, receivedResponse)
44947230 963 self.checkResponseNoEDNS(response, receivedResponse)
d27309a9 964
d2c3ef4b
RG
965class 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
968Configuration 'configs/dnsdist_TestDOHOverHTTPNGHTTP2.conf' OK!
969""" % (DOHOverHTTP._dohServerPort)
970
971class 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
974Configuration 'configs/dnsdist_TestDOHOverHTTPH2O.conf' OK!
975""" % (DOHOverHTTP._dohServerPort)
976
977class DOHWithCache(object):
d27309a9
RG
978
979 _serverKey = 'server.key'
980 _serverCert = 'server.chain'
981 _serverName = 'tls.tests.dnsdist.org'
982 _caCert = 'ca.pem'
630eb526 983 _dohServerPort = pickAvailablePort()
a6f94324 984 _dohBaseURL = ("https://%s:%d/dns-query" % (_serverName, _dohServerPort))
d27309a9
RG
985 _config_template = """
986 newServer{address="127.0.0.1:%s"}
987
d2c3ef4b 988 addDOHLocal("127.0.0.1:%s", "%s", "%s", '/dns-query', {library='%s'})
d27309a9
RG
989
990 pc = newPacketCache(100, {maxTTL=86400, minTTL=1})
991 getPool(""):setCache(pc)
992 """
d2c3ef4b 993 _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey', '_dohLibrary']
d27309a9
RG
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)
4bfebc93 1021 self.assertEqual(len(response.to_wire()), 4096)
d27309a9
RG
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
4bfebc93 1028 self.assertEqual(expectedQuery, receivedQuery)
d27309a9 1029 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
4bfebc93 1030 self.assertEqual(response, receivedResponse)
0026abf9 1031 self.checkHasHeader('cache-control', 'max-age=3600')
d27309a9
RG
1032
1033 for _ in range(numberOfQueries):
1034 (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, caFile=self._caCert, useQueue=False)
4bfebc93 1035 self.assertEqual(receivedResponse, response)
0026abf9
RG
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)
4bfebc93 1041 self.assertEqual(receivedResponse, response)
0026abf9
RG
1042 self.checkHasHeader('cache-control', 'max-age=' + str(receivedResponse.answer[0].ttl))
1043
2e74917d
RG
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
7e8cef3c
RG
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')
8ac88d69 1110 query.id = 42
7e8cef3c 1111 expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096)
8ac88d69 1112 expectedQuery.id = 42
7e8cef3c
RG
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)
fda32c1c 1127 # first query, received by the responder over UDP
7e8cef3c
RG
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
fda32c1c 1137 # check the second query, received by the responder over TCP
7e8cef3c
RG
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
d2c3ef4b
RG
1186class TestDOHWithCacheNGHTTP2(DOHWithCache, DNSDistDOHTest):
1187 _dohLibrary = 'nghttp2'
1188 _verboseMode = True
1189
1190class TestDOHWithCacheH2O(DOHWithCache, DNSDistDOHTest):
1191 _dohLibrary = 'h2o'
1192
1193class DOHWithoutCacheControl(object):
0026abf9
RG
1194
1195 _serverKey = 'server.key'
1196 _serverCert = 'server.chain'
1197 _serverName = 'tls.tests.dnsdist.org'
1198 _caCert = 'ca.pem'
630eb526 1199 _dohServerPort = pickAvailablePort()
0026abf9
RG
1200 _dohBaseURL = ("https://%s:%d/" % (_serverName, _dohServerPort))
1201 _config_template = """
1202 newServer{address="127.0.0.1:%s"}
1203
d2c3ef4b 1204 addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" }, {sendCacheControlHeaders=false, library='%s'})
0026abf9 1205 """
d2c3ef4b 1206 _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey', '_dohLibrary']
0026abf9
RG
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
4bfebc93 1229 self.assertEqual(expectedQuery, receivedQuery)
0026abf9
RG
1230 self.checkNoHeader('cache-control')
1231 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
4bfebc93 1232 self.assertEqual(response, receivedResponse)
f7e6a5ce 1233
d2c3ef4b
RG
1234class TestDOHWithoutCacheControlNGHTTP2(DOHWithoutCacheControl, DNSDistDOHTest):
1235 _dohLibrary = 'nghttp2'
f7e6a5ce 1236
d2c3ef4b
RG
1237class TestDOHWithoutCacheControlH2O(DOHWithoutCacheControl, DNSDistDOHTest):
1238 _dohLibrary = 'h2o'
1239
1240class DOHFFI(object):
f7e6a5ce
RG
1241 _serverKey = 'server.key'
1242 _serverCert = 'server.chain'
1243 _serverName = 'tls.tests.dnsdist.org'
1244 _caCert = 'ca.pem'
630eb526 1245 _dohServerPort = pickAvailablePort()
f7e6a5ce
RG
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
d2c3ef4b 1252 addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" }, {customResponseHeaders={["access-control-allow-origin"]="*",["user-agent"]="derp",["UPPERCASE"]="VaLuE"}, keepIncomingHeaders=true, library='%s'})
f7e6a5ce
RG
1253
1254 local ffi = require("ffi")
1255
f7e6a5ce
RG
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
a291bc0d
RG
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)
f7e6a5ce
RG
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
e13f437e
RG
1276 local response = 'It works!'
1277 ffi.C.dnsdist_ffi_dnsquestion_set_http_response(dq, 200, response, #response, 'text/plain')
f7e6a5ce
RG
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 """
d2c3ef4b 1285 _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey', '_dohLibrary', '_serverName', '_dohServerPort']
f7e6a5ce
RG
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)
4bfebc93
CH
1297 self.assertEqual(receivedResponse, b'It works!')
1298 self.assertEqual(self._rcode, 200)
f7e6a5ce
RG
1299 self.assertTrue('content-type: text/plain' in self._response_headers.decode())
1300
d2c3ef4b
RG
1301class TestDOHFFINGHTTP2(DOHFFI, DNSDistDOHTest):
1302 _dohLibrary = 'nghttp2'
1303
1304class TestDOHFFIH2O(DOHFFI, DNSDistDOHTest):
1305 _dohLibrary = 'h2o'
8b5f4644 1306
d2c3ef4b 1307class DOHForwardedFor(object):
8b5f4644
RG
1308 _serverKey = 'server.key'
1309 _serverCert = 'server.chain'
1310 _serverName = 'tls.tests.dnsdist.org'
1311 _caCert = 'ca.pem'
630eb526 1312 _dohServerPort = pickAvailablePort()
8b5f4644
RG
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')
d2c3ef4b 1318 addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" }, {trustForwardedForHeader=true, library='%s'})
9b703b51
RG
1319 -- Set a maximum number of TCP connections per client, to exercise
1320 -- that code along with X-Forwarded-For support
1321 setMaxTCPConnectionsPerClient(2)
8b5f4644 1322 """
d2c3ef4b 1323 _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey', '_dohLibrary']
8b5f4644
RG
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
4bfebc93 1346 self.assertEqual(expectedQuery, receivedQuery)
8b5f4644 1347 self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery)
4bfebc93 1348 self.assertEqual(response, receivedResponse)
8b5f4644
RG
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
4bfebc93 1369 self.assertEqual(self._rcode, 403)
7e8a05fa 1370 self.assertEqual(receivedResponse, b'DoH query not allowed because of ACL')
8b5f4644 1371
d2c3ef4b
RG
1372class TestDOHForwardedForNGHTTP2(DOHForwardedFor, DNSDistDOHTest):
1373 _dohLibrary = 'nghttp2'
1374
1375class TestDOHForwardedForH2O(DOHForwardedFor, DNSDistDOHTest):
1376 _dohLibrary = 'h2o'
1377
1378class DOHForwardedForNoTrusted(object):
8b5f4644
RG
1379
1380 _serverKey = 'server.key'
1381 _serverCert = 'server.chain'
1382 _serverName = 'tls.tests.dnsdist.org'
1383 _caCert = 'ca.pem'
630eb526 1384 _dohServerPort = pickAvailablePort()
8b5f4644
RG
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')
d2c3ef4b 1390 addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" }, {earlyACLDrop=true, library='%s'})
8b5f4644 1391 """
d2c3ef4b 1392 _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey', '_dohLibrary']
8b5f4644
RG
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
7e8a05fa
RG
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
8b5f4644 1418
7e8a05fa 1419 self.assertTrue(dropped)
d4d57f56 1420
d2c3ef4b
RG
1421class TestDOHForwardedForNoTrustedNGHTTP2(DOHForwardedForNoTrusted, DNSDistDOHTest):
1422 _dohLibrary = 'nghttp2'
1423
1424class TestDOHForwardedForNoTrustedH2O(DOHForwardedForNoTrusted, DNSDistDOHTest):
1425 _dohLibrary = 'h2o'
1426
1427class DOHFrontendLimits(object):
d4d57f56
RG
1428
1429 # this test suite uses a different responder port
1430 # because it uses a different health check configuration
630eb526 1431 _testServerPort = pickAvailablePort()
d4d57f56
RG
1432 _answerUnexpected = True
1433
1434 _serverKey = 'server.key'
1435 _serverCert = 'server.chain'
1436 _serverName = 'tls.tests.dnsdist.org'
1437 _caCert = 'ca.pem'
630eb526 1438 _dohServerPort = pickAvailablePort()
d4d57f56 1439 _dohBaseURL = ("https://%s:%d/" % (_serverName, _dohServerPort))
d4d57f56
RG
1440 _skipListeningOnCL = True
1441 _maxTCPConnsPerDOHFrontend = 5
1442 _config_template = """
1443 newServer{address="127.0.0.1:%s"}
d2c3ef4b 1444 addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" }, { maxConcurrentTCPConnections=%d, library='%s' })
d4d57f56 1445 """
d2c3ef4b 1446 _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey', '_maxTCPConnsPerDOHFrontend', '_dohLibrary']
1953ab6c
RG
1447 _alternateListeningAddr = '127.0.0.1'
1448 _alternateListeningPort = _dohServerPort
d4d57f56
RG
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:
d2c3ef4b
RG
1460 alpn = []
1461 if self._dohLibrary != 'h2o':
1462 alpn.append('h2')
1463 conns.append(self.openTLSConnection(self._dohServerPort, self._serverName, self._caCert, alpn=alpn))
d4d57f56
RG
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)
7d808ff4 1495
d2c3ef4b
RG
1496class TestDOHFrontendLimitsNGHTTP2(DOHFrontendLimits, DNSDistDOHTest):
1497 _dohLibrary = 'nghttp2'
1498
1499class TestDOHFrontendLimitsH2O(DOHFrontendLimits, DNSDistDOHTest):
1500 _dohLibrary = 'h2o'
1501
1502class Protocols(object):
7d808ff4
RG
1503 _serverKey = 'server.key'
1504 _serverCert = 'server.chain'
1505 _serverName = 'tls.tests.dnsdist.org'
1506 _caCert = 'ca.pem'
630eb526 1507 _dohServerPort = pickAvailablePort()
7d808ff4
RG
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"}
d2c3ef4b 1521 addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" }, {library='%s'})
7d808ff4 1522 """
d2c3ef4b 1523 _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey', '_dohLibrary']
7d808ff4
RG
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)
5ac11505 1542
d2c3ef4b
RG
1543class TestProtocolsNGHTTP2(Protocols, DNSDistDOHTest):
1544 _dohLibrary = 'nghttp2'
1545
1546class TestProtocolsH2O(Protocols, DNSDistDOHTest):
1547 _dohLibrary = 'h2o'
1548
1549class DOHWithPKCS12Cert(object):
5ac11505
CHB
1550 _serverCert = 'server.p12'
1551 _pkcs12Password = 'passw0rd'
1552 _serverName = 'tls.tests.dnsdist.org'
1553 _caCert = 'ca.pem'
630eb526 1554 _dohServerPort = pickAvailablePort()
5ac11505
CHB
1555 _dohBaseURL = ("https://%s:%d/" % (_serverName, _dohServerPort))
1556 _config_template = """
1557 newServer{address="127.0.0.1:%s"}
1558 cert=newTLSCertificate("%s", {password="%s"})
d2c3ef4b 1559 addDOHLocal("127.0.0.1:%s", cert, "", { "/" }, {library='%s'})
5ac11505 1560 """
d2c3ef4b 1561 _config_params = ['_testServerPort', '_serverCert', '_pkcs12Password', '_dohServerPort', '_dohLibrary']
5ac11505 1562
d2c3ef4b 1563 def testPKCS12DOH(self):
5ac11505 1564 """
890add84 1565 DoH: Test Simple DOH Query with a password protected PKCS12 file configured
5ac11505
CHB
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)
fda32c1c 1585
d2c3ef4b
RG
1586class TestDOHWithPKCS12CertNGHTTP2(DOHWithPKCS12Cert, DNSDistDOHTest):
1587 _dohLibrary = 'nghttp2'
1588
1589class TestDOHWithPKCS12CertH2O(DOHWithPKCS12Cert, DNSDistDOHTest):
1590 _dohLibrary = 'h2o'
1591
1592class DOHForwardedToTCPOnly(object):
fda32c1c
RG
1593 _serverKey = 'server.key'
1594 _serverCert = 'server.chain'
1595 _serverName = 'tls.tests.dnsdist.org'
1596 _caCert = 'ca.pem'
630eb526 1597 _dohServerPort = pickAvailablePort()
fda32c1c
RG
1598 _dohBaseURL = ("https://%s:%d/" % (_serverName, _dohServerPort))
1599 _config_template = """
1600 newServer{address="127.0.0.1:%s", tcpOnly=true}
d2c3ef4b 1601 addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" }, {library='%s'})
fda32c1c 1602 """
d2c3ef4b 1603 _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey', '_dohLibrary']
fda32c1c
RG
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)
4ec28674 1626
d2c3ef4b
RG
1627class TestDOHForwardedToTCPOnlyNGHTTP2(DOHForwardedToTCPOnly, DNSDistDOHTest):
1628 _dohLibrary = 'nghttp2'
1629
1630class TestDOHForwardedToTCPOnlyH2O(DOHForwardedToTCPOnly, DNSDistDOHTest):
1631 _dohLibrary = 'h2o'
1632
1633class DOHLimits(object):
4ec28674
RG
1634 _serverName = 'tls.tests.dnsdist.org'
1635 _caCert = 'ca.pem'
630eb526 1636 _dohServerPort = pickAvailablePort()
4ec28674
RG
1637 _dohBaseURL = ("https://%s:%d/" % (_serverName, _dohServerPort))
1638 _serverKey = 'server.key'
1639 _serverCert = 'server.chain'
1640 _maxTCPConnsPerClient = 3
1641 _config_template = """
d2c3ef4b
RG
1642 newServer{address="127.0.0.1:%d"}
1643 addDOHLocal("127.0.0.1:%d", "%s", "%s", { "/" }, {library='%s'})
1644 setMaxTCPConnectionsPerClient(%d)
4ec28674 1645 """
d2c3ef4b 1646 _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey', '_dohLibrary', '_maxTCPConnsPerClient']
4ec28674
RG
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)
d2c3ef4b
RG
1686
1687class TestDOHLimitsNGHTTP2(DOHLimits, DNSDistDOHTest):
1688 _dohLibrary = 'nghttp2'
1689
1690class TestDOHLimitsH2O(DOHLimits, DNSDistDOHTest):
1691 _dohLibrary = 'h2o'