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