]>
Commit | Line | Data |
---|---|---|
10535d88 RG |
1 | #!/usr/bin/env python |
2 | import base64 | |
3 | import dns | |
13291274 RG |
4 | import os |
5 | import unittest | |
10535d88 RG |
6 | import clientsubnetoption |
7 | from dnsdisttests import DNSDistTest | |
8 | ||
9 | import pycurl | |
811872fb | 10 | from io import BytesIO |
10535d88 RG |
11 | #from hyper import HTTP20Connection |
12 | #from hyper.ssl_compat import SSLContext, PROTOCOL_TLSv1_2 | |
13 | ||
13291274 | 14 | @unittest.skipIf('SKIP_DOH_TESTS' in os.environ, 'DNS over HTTPS tests are disabled') |
10535d88 RG |
15 | class DNSDistDOHTest(DNSDistTest): |
16 | ||
17 | @classmethod | |
47225117 RG |
18 | def getDOHGetURL(cls, baseurl, query, rawQuery=False): |
19 | if rawQuery: | |
20 | wire = query | |
21 | else: | |
22 | wire = query.to_wire() | |
10535d88 RG |
23 | param = base64.urlsafe_b64encode(wire).decode('UTF8').rstrip('=') |
24 | return baseurl + "?dns=" + param | |
25 | ||
26 | @classmethod | |
27 | def openDOHConnection(cls, port, caFile, timeout=2.0): | |
28 | conn = pycurl.Curl() | |
29 | conn.setopt(pycurl.HTTP_VERSION, pycurl.CURL_HTTP_VERSION_2) | |
30 | ||
31 | conn.setopt(pycurl.HTTPHEADER, ["Content-type: application/dns-message", | |
32 | "Accept: application/dns-message"]) | |
33 | return conn | |
34 | ||
35 | @classmethod | |
44947230 | 36 | def sendDOHQuery(cls, port, servername, baseurl, query, response=None, timeout=2.0, caFile=None, useQueue=True, rawQuery=False, rawResponse=False, customHeaders=[], useHTTPS=True): |
47225117 | 37 | url = cls.getDOHGetURL(baseurl, query, rawQuery) |
10535d88 | 38 | conn = cls.openDOHConnection(port, caFile=caFile, timeout=timeout) |
811872fb | 39 | response_headers = BytesIO() |
10535d88 RG |
40 | #conn.setopt(pycurl.VERBOSE, True) |
41 | conn.setopt(pycurl.URL, url) | |
42 | conn.setopt(pycurl.RESOLVE, ["%s:%d:127.0.0.1" % (servername, port)]) | |
44947230 RG |
43 | if useHTTPS: |
44 | conn.setopt(pycurl.SSL_VERIFYPEER, 1) | |
45 | conn.setopt(pycurl.SSL_VERIFYHOST, 2) | |
46 | if caFile: | |
47 | conn.setopt(pycurl.CAINFO, caFile) | |
48 | ||
9ba32868 | 49 | conn.setopt(pycurl.HTTPHEADER, customHeaders) |
ee01507f | 50 | conn.setopt(pycurl.HEADERFUNCTION, response_headers.write) |
10535d88 RG |
51 | |
52 | if response: | |
53 | cls._toResponderQueue.put(response, True, timeout) | |
54 | ||
55 | receivedQuery = None | |
56 | message = None | |
ee01507f | 57 | cls._response_headers = '' |
10535d88 | 58 | data = conn.perform_rb() |
9676d2a9 RG |
59 | cls._rcode = conn.getinfo(pycurl.RESPONSE_CODE) |
60 | if cls._rcode == 200 and not rawResponse: | |
10535d88 | 61 | message = dns.message.from_wire(data) |
9676d2a9 RG |
62 | elif rawResponse: |
63 | message = data | |
10535d88 RG |
64 | |
65 | if useQueue and not cls._fromResponderQueue.empty(): | |
66 | receivedQuery = cls._fromResponderQueue.get(True, timeout) | |
67 | ||
ee01507f | 68 | cls._response_headers = response_headers.getvalue() |
10535d88 RG |
69 | return (receivedQuery, message) |
70 | ||
b1e527ad | 71 | @classmethod |
44947230 | 72 | def sendDOHPostQuery(cls, port, servername, baseurl, query, response=None, timeout=2.0, caFile=None, useQueue=True, rawQuery=False, rawResponse=False, customHeaders=[], useHTTPS=True): |
b1e527ad RG |
73 | url = baseurl |
74 | conn = cls.openDOHConnection(port, caFile=caFile, timeout=timeout) | |
9676d2a9 | 75 | response_headers = BytesIO() |
b1e527ad RG |
76 | #conn.setopt(pycurl.VERBOSE, True) |
77 | conn.setopt(pycurl.URL, url) | |
78 | conn.setopt(pycurl.RESOLVE, ["%s:%d:127.0.0.1" % (servername, port)]) | |
44947230 RG |
79 | if useHTTPS: |
80 | conn.setopt(pycurl.SSL_VERIFYPEER, 1) | |
81 | conn.setopt(pycurl.SSL_VERIFYHOST, 2) | |
82 | if caFile: | |
83 | conn.setopt(pycurl.CAINFO, caFile) | |
84 | ||
9676d2a9 RG |
85 | conn.setopt(pycurl.HTTPHEADER, customHeaders) |
86 | conn.setopt(pycurl.HEADERFUNCTION, response_headers.write) | |
b1e527ad RG |
87 | conn.setopt(pycurl.POST, True) |
88 | data = query | |
89 | if not rawQuery: | |
90 | data = data.to_wire() | |
91 | ||
92 | conn.setopt(pycurl.POSTFIELDS, data) | |
93 | ||
b1e527ad RG |
94 | if response: |
95 | cls._toResponderQueue.put(response, True, timeout) | |
96 | ||
97 | receivedQuery = None | |
98 | message = None | |
9676d2a9 | 99 | cls._response_headers = '' |
b1e527ad | 100 | data = conn.perform_rb() |
9676d2a9 RG |
101 | cls._rcode = conn.getinfo(pycurl.RESPONSE_CODE) |
102 | if cls._rcode == 200 and not rawResponse: | |
b1e527ad | 103 | message = dns.message.from_wire(data) |
9676d2a9 RG |
104 | elif rawResponse: |
105 | message = data | |
b1e527ad RG |
106 | |
107 | if useQueue and not cls._fromResponderQueue.empty(): | |
108 | receivedQuery = cls._fromResponderQueue.get(True, timeout) | |
109 | ||
9676d2a9 | 110 | cls._response_headers = response_headers.getvalue() |
b1e527ad RG |
111 | return (receivedQuery, message) |
112 | ||
13291274 RG |
113 | @classmethod |
114 | def setUpClass(cls): | |
115 | ||
116 | # for some reason, @unittest.skipIf() is not applied to derived classes with some versions of Python | |
117 | if 'SKIP_DOH_TESTS' in os.environ: | |
118 | raise unittest.SkipTest('DNS over HTTPS tests are disabled') | |
119 | ||
120 | cls.startResponders() | |
121 | cls.startDNSDist() | |
122 | cls.setUpSockets() | |
123 | ||
124 | print("Launching tests..") | |
125 | ||
10535d88 RG |
126 | # @classmethod |
127 | # def openDOHConnection(cls, port, caFile, timeout=2.0): | |
128 | # sslctx = SSLContext(PROTOCOL_TLSv1_2) | |
129 | # sslctx.load_verify_locations(caFile) | |
130 | # return HTTP20Connection('127.0.0.1', port=port, secure=True, timeout=timeout, ssl_context=sslctx, force_proto='h2') | |
131 | ||
132 | # @classmethod | |
133 | # def sendDOHQueryOverConnection(cls, conn, baseurl, query, response=None, timeout=2.0): | |
134 | # url = cls.getDOHGetURL(baseurl, query) | |
135 | ||
136 | # if response: | |
137 | # cls._toResponderQueue.put(response, True, timeout) | |
138 | ||
139 | # conn.request('GET', url) | |
140 | ||
141 | # @classmethod | |
142 | # def recvDOHResponseOverConnection(cls, conn, useQueue=False, timeout=2.0): | |
143 | # message = None | |
144 | # data = conn.get_response() | |
145 | # if data: | |
146 | # data = data.read() | |
147 | # if data: | |
148 | # message = dns.message.from_wire(data) | |
149 | ||
150 | # if useQueue and not cls._fromResponderQueue.empty(): | |
151 | # receivedQuery = cls._fromResponderQueue.get(True, timeout) | |
152 | # return (receivedQuery, message) | |
153 | # else: | |
154 | # return message | |
155 | ||
156 | class TestDOH(DNSDistDOHTest): | |
157 | ||
158 | _serverKey = 'server.key' | |
159 | _serverCert = 'server.chain' | |
160 | _serverName = 'tls.tests.dnsdist.org' | |
161 | _caCert = 'ca.pem' | |
162 | _dohServerPort = 8443 | |
ee01507f CR |
163 | _customResponseHeader1 = 'access-control-allow-origin: *' |
164 | _customResponseHeader2 = 'user-agent: derp' | |
10535d88 RG |
165 | _dohBaseURL = ("https://%s:%d/" % (_serverName, _dohServerPort)) |
166 | _config_template = """ | |
167 | newServer{address="127.0.0.1:%s"} | |
ee01507f | 168 | |
cf3e149b | 169 | addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" }, {customResponseHeaders={["access-control-allow-origin"]="*",["user-agent"]="derp",["UPPERCASE"]="VaLuE"}}) |
28b56482 | 170 | dohFE = getDOHFrontend(0) |
e1b72559 | 171 | dohFE:setResponsesMap({newDOHResponseMapEntry('^/coffee$', 418, 'C0FFEE', {['FoO']='bar'})}) |
10535d88 RG |
172 | |
173 | addAction("drop.doh.tests.powerdns.com.", DropAction()) | |
174 | addAction("refused.doh.tests.powerdns.com.", RCodeAction(DNSRCode.REFUSED)) | |
175 | addAction("spoof.doh.tests.powerdns.com.", SpoofAction("1.2.3.4")) | |
9ba32868 RG |
176 | addAction(HTTPHeaderRule("X-PowerDNS", "^[a]{5}$"), SpoofAction("2.3.4.5")) |
177 | addAction(HTTPPathRule("/PowerDNS"), SpoofAction("3.4.5.6")) | |
9676d2a9 RG |
178 | addAction(HTTPPathRegexRule("^/PowerDNS-[0-9]"), SpoofAction("6.7.8.9")) |
179 | addAction("http-status-action.doh.tests.powerdns.com.", HTTPStatusAction(200, "Plaintext answer", "text/plain")) | |
180 | addAction("http-status-action-redirect.doh.tests.powerdns.com.", HTTPStatusAction(307, "https://doh.powerdns.org")) | |
181 | ||
182 | function dohHandler(dq) | |
183 | if dq:getHTTPScheme() == 'https' and dq:getHTTPHost() == '%s:%d' and dq:getHTTPPath() == '/' and dq:getHTTPQueryString() == '' then | |
184 | local foundct = false | |
185 | for key,value in pairs(dq:getHTTPHeaders()) do | |
186 | if key == 'content-type' and value == 'application/dns-message' then | |
187 | foundct = true | |
188 | break | |
189 | end | |
190 | end | |
191 | if foundct then | |
192 | dq:setHTTPResponse(200, 'It works!', 'text/plain') | |
193 | dq.dh:setQR(true) | |
194 | return DNSAction.HeaderModify | |
195 | end | |
196 | end | |
197 | return DNSAction.None | |
198 | end | |
199 | addAction("http-lua.doh.tests.powerdns.com.", LuaAction(dohHandler)) | |
10535d88 | 200 | """ |
9676d2a9 | 201 | _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey', '_serverName', '_dohServerPort'] |
10535d88 RG |
202 | |
203 | def testDOHSimple(self): | |
204 | """ | |
205 | DOH: Simple query | |
206 | """ | |
207 | name = 'simple.doh.tests.powerdns.com.' | |
208 | query = dns.message.make_query(name, 'A', 'IN', use_edns=False) | |
209 | query.id = 0 | |
210 | expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096) | |
211 | expectedQuery.id = 0 | |
212 | response = dns.message.make_response(query) | |
213 | rrset = dns.rrset.from_text(name, | |
214 | 3600, | |
215 | dns.rdataclass.IN, | |
216 | dns.rdatatype.A, | |
217 | '127.0.0.1') | |
218 | response.answer.append(rrset) | |
219 | ||
220 | (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert) | |
221 | self.assertTrue(receivedQuery) | |
222 | self.assertTrue(receivedResponse) | |
223 | receivedQuery.id = expectedQuery.id | |
224 | self.assertEquals(expectedQuery, receivedQuery) | |
811872fb RG |
225 | self.assertTrue((self._customResponseHeader1) in self._response_headers.decode()) |
226 | self.assertTrue((self._customResponseHeader2) in self._response_headers.decode()) | |
cf3e149b RG |
227 | self.assertFalse(('UPPERCASE: VaLuE' in self._response_headers.decode())) |
228 | self.assertTrue(('uppercase: VaLuE' in self._response_headers.decode())) | |
10535d88 RG |
229 | self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery) |
230 | self.assertEquals(response, receivedResponse) | |
231 | ||
b1e527ad RG |
232 | def testDOHSimplePOST(self): |
233 | """ | |
234 | DOH: Simple POST query | |
235 | """ | |
236 | name = 'simple-post.doh.tests.powerdns.com.' | |
237 | query = dns.message.make_query(name, 'A', 'IN', use_edns=False) | |
238 | query.id = 0 | |
239 | expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096) | |
240 | expectedQuery.id = 0 | |
241 | response = dns.message.make_response(query) | |
242 | rrset = dns.rrset.from_text(name, | |
243 | 3600, | |
244 | dns.rdataclass.IN, | |
245 | dns.rdatatype.A, | |
246 | '127.0.0.1') | |
247 | response.answer.append(rrset) | |
248 | ||
249 | (receivedQuery, receivedResponse) = self.sendDOHPostQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert) | |
250 | self.assertTrue(receivedQuery) | |
251 | self.assertTrue(receivedResponse) | |
252 | receivedQuery.id = expectedQuery.id | |
253 | self.assertEquals(expectedQuery, receivedQuery) | |
254 | self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery) | |
255 | self.assertEquals(response, receivedResponse) | |
256 | ||
10535d88 RG |
257 | def testDOHExistingEDNS(self): |
258 | """ | |
259 | DOH: Existing EDNS | |
260 | """ | |
261 | name = 'existing-edns.doh.tests.powerdns.com.' | |
262 | query = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=8192) | |
263 | query.id = 0 | |
264 | response = dns.message.make_response(query) | |
265 | rrset = dns.rrset.from_text(name, | |
266 | 3600, | |
267 | dns.rdataclass.IN, | |
268 | dns.rdatatype.A, | |
269 | '127.0.0.1') | |
270 | response.answer.append(rrset) | |
271 | ||
272 | (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert) | |
273 | self.assertTrue(receivedQuery) | |
274 | self.assertTrue(receivedResponse) | |
275 | receivedQuery.id = query.id | |
276 | self.assertEquals(query, receivedQuery) | |
277 | self.assertEquals(response, receivedResponse) | |
278 | self.checkQueryEDNSWithoutECS(query, receivedQuery) | |
279 | self.checkResponseEDNSWithoutECS(response, receivedResponse) | |
280 | ||
281 | def testDOHExistingECS(self): | |
282 | """ | |
283 | DOH: Existing EDNS Client Subnet | |
284 | """ | |
285 | name = 'existing-ecs.doh.tests.powerdns.com.' | |
286 | ecso = clientsubnetoption.ClientSubnetOption('1.2.3.4') | |
287 | rewrittenEcso = clientsubnetoption.ClientSubnetOption('127.0.0.1', 24) | |
288 | query = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=512, options=[ecso], want_dnssec=True) | |
289 | query.id = 0 | |
290 | response = dns.message.make_response(query) | |
291 | response.use_edns(edns=True, payload=4096, options=[rewrittenEcso]) | |
292 | rrset = dns.rrset.from_text(name, | |
293 | 3600, | |
294 | dns.rdataclass.IN, | |
295 | dns.rdatatype.A, | |
296 | '127.0.0.1') | |
297 | response.answer.append(rrset) | |
298 | ||
299 | (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert) | |
300 | self.assertTrue(receivedQuery) | |
301 | self.assertTrue(receivedResponse) | |
302 | receivedQuery.id = query.id | |
303 | self.assertEquals(query, receivedQuery) | |
304 | self.assertEquals(response, receivedResponse) | |
305 | self.checkQueryEDNSWithECS(query, receivedQuery) | |
306 | self.checkResponseEDNSWithECS(response, receivedResponse) | |
307 | ||
308 | def testDropped(self): | |
309 | """ | |
310 | DOH: Dropped query | |
311 | """ | |
312 | name = 'drop.doh.tests.powerdns.com.' | |
313 | query = dns.message.make_query(name, 'A', 'IN') | |
314 | (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, caFile=self._caCert, query=query, response=None, useQueue=False) | |
10535d88 RG |
315 | self.assertEquals(receivedResponse, None) |
316 | ||
317 | def testRefused(self): | |
318 | """ | |
319 | DOH: Refused | |
320 | """ | |
321 | name = 'refused.doh.tests.powerdns.com.' | |
322 | query = dns.message.make_query(name, 'A', 'IN') | |
323 | query.id = 0 | |
7af22479 | 324 | query.flags &= ~dns.flags.RD |
10535d88 RG |
325 | expectedResponse = dns.message.make_response(query) |
326 | expectedResponse.set_rcode(dns.rcode.REFUSED) | |
327 | ||
328 | (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, caFile=self._caCert, query=query, response=None, useQueue=False) | |
329 | self.assertEquals(receivedResponse, expectedResponse) | |
330 | ||
331 | def testSpoof(self): | |
332 | """ | |
333 | DOH: Spoofed | |
334 | """ | |
335 | name = 'spoof.doh.tests.powerdns.com.' | |
336 | query = dns.message.make_query(name, 'A', 'IN') | |
337 | query.id = 0 | |
338 | query.flags &= ~dns.flags.RD | |
339 | expectedResponse = dns.message.make_response(query) | |
340 | rrset = dns.rrset.from_text(name, | |
341 | 3600, | |
342 | dns.rdataclass.IN, | |
343 | dns.rdatatype.A, | |
344 | '1.2.3.4') | |
345 | expectedResponse.answer.append(rrset) | |
346 | ||
347 | (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, caFile=self._caCert, query=query, response=None, useQueue=False) | |
348 | self.assertEquals(receivedResponse, expectedResponse) | |
349 | ||
47225117 RG |
350 | def testDOHInvalid(self): |
351 | """ | |
352 | DOH: Invalid query | |
353 | """ | |
354 | name = 'invalid.doh.tests.powerdns.com.' | |
355 | invalidQuery = dns.message.make_query(name, 'A', 'IN', use_edns=False) | |
356 | invalidQuery.id = 0 | |
357 | # first an invalid query | |
358 | invalidQuery = invalidQuery.to_wire() | |
359 | invalidQuery = invalidQuery[:-5] | |
360 | (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, caFile=self._caCert, query=invalidQuery, response=None, useQueue=False, rawQuery=True) | |
361 | self.assertEquals(receivedResponse, None) | |
362 | ||
363 | # and now a valid one | |
364 | query = dns.message.make_query(name, 'A', 'IN', use_edns=False) | |
365 | query.id = 0 | |
366 | expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096) | |
367 | expectedQuery.id = 0 | |
368 | response = dns.message.make_response(query) | |
369 | rrset = dns.rrset.from_text(name, | |
370 | 3600, | |
371 | dns.rdataclass.IN, | |
372 | dns.rdatatype.A, | |
373 | '127.0.0.1') | |
374 | response.answer.append(rrset) | |
375 | (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert) | |
376 | self.assertTrue(receivedQuery) | |
377 | self.assertTrue(receivedResponse) | |
378 | receivedQuery.id = expectedQuery.id | |
379 | self.assertEquals(expectedQuery, receivedQuery) | |
380 | self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery) | |
381 | self.assertEquals(response, receivedResponse) | |
10535d88 | 382 | |
2cb8efb1 RG |
383 | def testDOHWithoutQuery(self): |
384 | """ | |
385 | DOH: Empty GET query | |
386 | """ | |
387 | name = 'empty-get.doh.tests.powerdns.com.' | |
388 | url = self._dohBaseURL | |
389 | conn = self.openDOHConnection(self._dohServerPort, self._caCert, timeout=2.0) | |
390 | conn.setopt(pycurl.URL, url) | |
391 | conn.setopt(pycurl.RESOLVE, ["%s:%d:127.0.0.1" % (self._serverName, self._dohServerPort)]) | |
392 | conn.setopt(pycurl.SSL_VERIFYPEER, 1) | |
393 | conn.setopt(pycurl.SSL_VERIFYHOST, 2) | |
394 | conn.setopt(pycurl.CAINFO, self._caCert) | |
395 | data = conn.perform_rb() | |
396 | rcode = conn.getinfo(pycurl.RESPONSE_CODE) | |
397 | self.assertEquals(rcode, 400) | |
398 | ||
b1e527ad RG |
399 | def testDOHEmptyPOST(self): |
400 | """ | |
401 | DOH: Empty POST query | |
402 | """ | |
403 | name = 'empty-post.doh.tests.powerdns.com.' | |
404 | ||
405 | (_, receivedResponse) = self.sendDOHPostQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query="", rawQuery=True, response=None, caFile=self._caCert) | |
406 | self.assertEquals(receivedResponse, None) | |
407 | ||
408 | # and now a valid one | |
409 | query = dns.message.make_query(name, 'A', 'IN', use_edns=False) | |
410 | query.id = 0 | |
411 | expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096) | |
412 | expectedQuery.id = 0 | |
413 | response = dns.message.make_response(query) | |
414 | rrset = dns.rrset.from_text(name, | |
415 | 3600, | |
416 | dns.rdataclass.IN, | |
417 | dns.rdatatype.A, | |
418 | '127.0.0.1') | |
419 | response.answer.append(rrset) | |
420 | (receivedQuery, receivedResponse) = self.sendDOHPostQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert) | |
421 | self.assertTrue(receivedQuery) | |
422 | self.assertTrue(receivedResponse) | |
423 | receivedQuery.id = expectedQuery.id | |
424 | self.assertEquals(expectedQuery, receivedQuery) | |
425 | self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery) | |
426 | self.assertEquals(response, receivedResponse) | |
427 | ||
9ba32868 RG |
428 | def testHeaderRule(self): |
429 | """ | |
430 | DOH: HeaderRule | |
431 | """ | |
432 | name = 'header-rule.doh.tests.powerdns.com.' | |
433 | query = dns.message.make_query(name, 'A', 'IN') | |
434 | query.id = 0 | |
435 | query.flags &= ~dns.flags.RD | |
436 | expectedResponse = dns.message.make_response(query) | |
437 | rrset = dns.rrset.from_text(name, | |
438 | 3600, | |
439 | dns.rdataclass.IN, | |
440 | dns.rdatatype.A, | |
441 | '2.3.4.5') | |
442 | expectedResponse.answer.append(rrset) | |
443 | ||
444 | # this header should match | |
445 | (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, caFile=self._caCert, query=query, response=None, useQueue=False, customHeaders=['x-powerdnS: aaaaa']) | |
446 | self.assertEquals(receivedResponse, expectedResponse) | |
447 | ||
448 | expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096) | |
449 | expectedQuery.flags &= ~dns.flags.RD | |
450 | expectedQuery.id = 0 | |
451 | response = dns.message.make_response(query) | |
452 | rrset = dns.rrset.from_text(name, | |
453 | 3600, | |
454 | dns.rdataclass.IN, | |
455 | dns.rdatatype.A, | |
456 | '127.0.0.1') | |
457 | response.answer.append(rrset) | |
458 | ||
459 | # this content of the header should NOT match | |
460 | (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert, customHeaders=['x-powerdnS: bbbbb']) | |
461 | self.assertTrue(receivedQuery) | |
462 | self.assertTrue(receivedResponse) | |
463 | receivedQuery.id = expectedQuery.id | |
464 | self.assertEquals(expectedQuery, receivedQuery) | |
465 | self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery) | |
466 | self.assertEquals(response, receivedResponse) | |
467 | ||
468 | def testHTTPPath(self): | |
469 | """ | |
470 | DOH: HTTPPath | |
471 | """ | |
472 | name = 'http-path.doh.tests.powerdns.com.' | |
473 | query = dns.message.make_query(name, 'A', 'IN') | |
474 | query.id = 0 | |
475 | query.flags &= ~dns.flags.RD | |
476 | expectedResponse = dns.message.make_response(query) | |
477 | rrset = dns.rrset.from_text(name, | |
478 | 3600, | |
479 | dns.rdataclass.IN, | |
480 | dns.rdatatype.A, | |
481 | '3.4.5.6') | |
482 | expectedResponse.answer.append(rrset) | |
483 | ||
484 | # this path should match | |
485 | (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL + 'PowerDNS', caFile=self._caCert, query=query, response=None, useQueue=False) | |
486 | self.assertEquals(receivedResponse, expectedResponse) | |
487 | ||
488 | expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096) | |
489 | expectedQuery.id = 0 | |
490 | expectedQuery.flags &= ~dns.flags.RD | |
491 | response = dns.message.make_response(query) | |
492 | rrset = dns.rrset.from_text(name, | |
493 | 3600, | |
494 | dns.rdataclass.IN, | |
495 | dns.rdatatype.A, | |
496 | '127.0.0.1') | |
497 | response.answer.append(rrset) | |
498 | ||
499 | # this path should NOT match | |
500 | (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL + "PowerDNS2", query, response=response, caFile=self._caCert) | |
501 | self.assertTrue(receivedQuery) | |
502 | self.assertTrue(receivedResponse) | |
503 | receivedQuery.id = expectedQuery.id | |
504 | self.assertEquals(expectedQuery, receivedQuery) | |
505 | self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery) | |
506 | self.assertEquals(response, receivedResponse) | |
507 | ||
9676d2a9 RG |
508 | def testHTTPPathRegex(self): |
509 | """ | |
510 | DOH: HTTPPathRegex | |
511 | """ | |
512 | name = 'http-path-regex.doh.tests.powerdns.com.' | |
513 | query = dns.message.make_query(name, 'A', 'IN') | |
514 | query.id = 0 | |
515 | query.flags &= ~dns.flags.RD | |
516 | expectedResponse = dns.message.make_response(query) | |
517 | rrset = dns.rrset.from_text(name, | |
518 | 3600, | |
519 | dns.rdataclass.IN, | |
520 | dns.rdatatype.A, | |
521 | '6.7.8.9') | |
522 | expectedResponse.answer.append(rrset) | |
523 | ||
524 | # this path should match | |
525 | (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL + 'PowerDNS-999', caFile=self._caCert, query=query, response=None, useQueue=False) | |
526 | self.assertEquals(receivedResponse, expectedResponse) | |
527 | ||
528 | expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096) | |
529 | expectedQuery.id = 0 | |
530 | expectedQuery.flags &= ~dns.flags.RD | |
531 | response = dns.message.make_response(query) | |
532 | rrset = dns.rrset.from_text(name, | |
533 | 3600, | |
534 | dns.rdataclass.IN, | |
535 | dns.rdatatype.A, | |
536 | '127.0.0.1') | |
537 | response.answer.append(rrset) | |
538 | ||
539 | # this path should NOT match | |
540 | (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL + "PowerDNS2", query, response=response, caFile=self._caCert) | |
541 | self.assertTrue(receivedQuery) | |
542 | self.assertTrue(receivedResponse) | |
543 | receivedQuery.id = expectedQuery.id | |
544 | self.assertEquals(expectedQuery, receivedQuery) | |
545 | self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery) | |
546 | self.assertEquals(response, receivedResponse) | |
547 | ||
548 | def testHTTPStatusAction200(self): | |
549 | """ | |
550 | DOH: HTTPStatusAction 200 OK | |
551 | """ | |
552 | name = 'http-status-action.doh.tests.powerdns.com.' | |
553 | query = dns.message.make_query(name, 'A', 'IN', use_edns=False) | |
554 | query.id = 0 | |
555 | ||
556 | (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, caFile=self._caCert, useQueue=False, rawResponse=True) | |
557 | self.assertTrue(receivedResponse) | |
558 | self.assertEquals(receivedResponse, b'Plaintext answer') | |
559 | self.assertEquals(self._rcode, 200) | |
560 | self.assertTrue('content-type: text/plain' in self._response_headers.decode()) | |
561 | ||
562 | def testHTTPStatusAction307(self): | |
563 | """ | |
564 | DOH: HTTPStatusAction 307 | |
565 | """ | |
566 | name = 'http-status-action-redirect.doh.tests.powerdns.com.' | |
567 | query = dns.message.make_query(name, 'A', 'IN', use_edns=False) | |
568 | query.id = 0 | |
569 | ||
570 | (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, caFile=self._caCert, useQueue=False, rawResponse=True) | |
571 | self.assertTrue(receivedResponse) | |
572 | self.assertEquals(self._rcode, 307) | |
573 | self.assertTrue('location: https://doh.powerdns.org' in self._response_headers.decode()) | |
574 | ||
575 | def testHTTPLuaResponse(self): | |
576 | """ | |
577 | DOH: Lua HTTP Response | |
578 | """ | |
579 | name = 'http-lua.doh.tests.powerdns.com.' | |
580 | query = dns.message.make_query(name, 'A', 'IN', use_edns=False) | |
581 | query.id = 0 | |
582 | ||
583 | (_, receivedResponse) = self.sendDOHPostQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, caFile=self._caCert, useQueue=False, rawResponse=True) | |
584 | self.assertTrue(receivedResponse) | |
585 | self.assertEquals(receivedResponse, b'It works!') | |
586 | self.assertEquals(self._rcode, 200) | |
587 | self.assertTrue('content-type: text/plain' in self._response_headers.decode()) | |
588 | ||
28b56482 RG |
589 | def testHTTPEarlyResponse(self): |
590 | """ | |
591 | DOH: HTTP Early Response | |
592 | """ | |
ded6907c | 593 | response_headers = BytesIO() |
28b56482 RG |
594 | url = self._dohBaseURL + 'coffee' |
595 | conn = self.openDOHConnection(self._dohServerPort, caFile=self._caCert, timeout=2.0) | |
596 | conn.setopt(pycurl.URL, url) | |
597 | conn.setopt(pycurl.RESOLVE, ["%s:%d:127.0.0.1" % (self._serverName, self._dohServerPort)]) | |
598 | conn.setopt(pycurl.SSL_VERIFYPEER, 1) | |
599 | conn.setopt(pycurl.SSL_VERIFYHOST, 2) | |
600 | conn.setopt(pycurl.CAINFO, self._caCert) | |
ded6907c | 601 | conn.setopt(pycurl.HEADERFUNCTION, response_headers.write) |
28b56482 RG |
602 | data = conn.perform_rb() |
603 | rcode = conn.getinfo(pycurl.RESPONSE_CODE) | |
9b2ef603 | 604 | headers = response_headers.getvalue().decode() |
28b56482 RG |
605 | |
606 | self.assertEquals(rcode, 418) | |
607 | self.assertEquals(data, b'C0FFEE') | |
ded6907c RG |
608 | self.assertIn('foo: bar', headers) |
609 | self.assertNotIn(self._customResponseHeader2, headers) | |
28b56482 | 610 | |
ded6907c | 611 | response_headers = BytesIO() |
28b56482 RG |
612 | conn = self.openDOHConnection(self._dohServerPort, caFile=self._caCert, timeout=2.0) |
613 | conn.setopt(pycurl.URL, url) | |
614 | conn.setopt(pycurl.RESOLVE, ["%s:%d:127.0.0.1" % (self._serverName, self._dohServerPort)]) | |
615 | conn.setopt(pycurl.SSL_VERIFYPEER, 1) | |
616 | conn.setopt(pycurl.SSL_VERIFYHOST, 2) | |
617 | conn.setopt(pycurl.CAINFO, self._caCert) | |
ded6907c | 618 | conn.setopt(pycurl.HEADERFUNCTION, response_headers.write) |
28b56482 RG |
619 | conn.setopt(pycurl.POST, True) |
620 | data = '' | |
621 | conn.setopt(pycurl.POSTFIELDS, data) | |
622 | ||
623 | data = conn.perform_rb() | |
624 | rcode = conn.getinfo(pycurl.RESPONSE_CODE) | |
9b2ef603 | 625 | headers = response_headers.getvalue().decode() |
28b56482 RG |
626 | self.assertEquals(rcode, 418) |
627 | self.assertEquals(data, b'C0FFEE') | |
ded6907c RG |
628 | self.assertIn('foo: bar', headers) |
629 | self.assertNotIn(self._customResponseHeader2, headers) | |
28b56482 | 630 | |
10535d88 RG |
631 | class TestDOHAddingECS(DNSDistDOHTest): |
632 | ||
633 | _serverKey = 'server.key' | |
634 | _serverCert = 'server.chain' | |
635 | _serverName = 'tls.tests.dnsdist.org' | |
636 | _caCert = 'ca.pem' | |
637 | _dohServerPort = 8443 | |
10535d88 RG |
638 | _dohBaseURL = ("https://%s:%d/" % (_serverName, _dohServerPort)) |
639 | _config_template = """ | |
640 | newServer{address="127.0.0.1:%s", useClientSubnet=true} | |
641 | addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" }) | |
642 | setECSOverride(true) | |
643 | """ | |
644 | _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey'] | |
645 | ||
646 | def testDOHSimple(self): | |
647 | """ | |
648 | DOH with ECS: Simple query | |
649 | """ | |
650 | name = 'simple.doh-ecs.tests.powerdns.com.' | |
651 | query = dns.message.make_query(name, 'A', 'IN', use_edns=False) | |
652 | query.id = 0 | |
653 | rewrittenEcso = clientsubnetoption.ClientSubnetOption('127.0.0.0', 24) | |
654 | expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096, options=[rewrittenEcso]) | |
655 | response = dns.message.make_response(query) | |
656 | rrset = dns.rrset.from_text(name, | |
657 | 3600, | |
658 | dns.rdataclass.IN, | |
659 | dns.rdatatype.A, | |
660 | '127.0.0.1') | |
661 | response.answer.append(rrset) | |
662 | ||
663 | (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert) | |
664 | self.assertTrue(receivedQuery) | |
665 | self.assertTrue(receivedResponse) | |
666 | expectedQuery.id = receivedQuery.id | |
667 | self.assertEquals(expectedQuery, receivedQuery) | |
668 | self.checkQueryEDNSWithECS(expectedQuery, receivedQuery) | |
669 | self.assertEquals(response, receivedResponse) | |
670 | self.checkResponseNoEDNS(response, receivedResponse) | |
671 | ||
672 | def testDOHExistingEDNS(self): | |
673 | """ | |
674 | DOH with ECS: Existing EDNS | |
675 | """ | |
676 | name = 'existing-edns.doh-ecs.tests.powerdns.com.' | |
677 | query = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=8192) | |
678 | query.id = 0 | |
679 | rewrittenEcso = clientsubnetoption.ClientSubnetOption('127.0.0.0', 24) | |
680 | expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=8192, options=[rewrittenEcso]) | |
681 | response = dns.message.make_response(query) | |
682 | rrset = dns.rrset.from_text(name, | |
683 | 3600, | |
684 | dns.rdataclass.IN, | |
685 | dns.rdatatype.A, | |
686 | '127.0.0.1') | |
687 | response.answer.append(rrset) | |
688 | ||
689 | (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert) | |
690 | self.assertTrue(receivedQuery) | |
691 | self.assertTrue(receivedResponse) | |
692 | receivedQuery.id = expectedQuery.id | |
693 | self.assertEquals(expectedQuery, receivedQuery) | |
694 | self.assertEquals(response, receivedResponse) | |
695 | self.checkQueryEDNSWithECS(expectedQuery, receivedQuery) | |
696 | self.checkResponseEDNSWithoutECS(response, receivedResponse) | |
697 | ||
698 | def testDOHExistingECS(self): | |
699 | """ | |
700 | DOH with ECS: Existing EDNS Client Subnet | |
701 | """ | |
702 | name = 'existing-ecs.doh-ecs.tests.powerdns.com.' | |
703 | ecso = clientsubnetoption.ClientSubnetOption('1.2.3.4') | |
704 | rewrittenEcso = clientsubnetoption.ClientSubnetOption('127.0.0.0', 24) | |
705 | query = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=512, options=[ecso], want_dnssec=True) | |
706 | query.id = 0 | |
707 | expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=512, options=[rewrittenEcso]) | |
708 | response = dns.message.make_response(query) | |
709 | response.use_edns(edns=True, payload=4096, options=[rewrittenEcso]) | |
710 | rrset = dns.rrset.from_text(name, | |
711 | 3600, | |
712 | dns.rdataclass.IN, | |
713 | dns.rdatatype.A, | |
714 | '127.0.0.1') | |
715 | response.answer.append(rrset) | |
716 | ||
717 | (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert) | |
718 | self.assertTrue(receivedQuery) | |
719 | self.assertTrue(receivedResponse) | |
720 | receivedQuery.id = expectedQuery.id | |
721 | self.assertEquals(expectedQuery, receivedQuery) | |
722 | self.assertEquals(response, receivedResponse) | |
723 | self.checkQueryEDNSWithECS(expectedQuery, receivedQuery) | |
724 | self.checkResponseEDNSWithECS(response, receivedResponse) | |
44947230 RG |
725 | |
726 | class TestDOHOverHTTP(DNSDistDOHTest): | |
727 | ||
728 | _dohServerPort = 8480 | |
729 | _serverName = 'tls.tests.dnsdist.org' | |
730 | _dohBaseURL = ("http://%s:%d/" % (_serverName, _dohServerPort)) | |
731 | _config_template = """ | |
732 | newServer{address="127.0.0.1:%s"} | |
733 | addDOHLocal("127.0.0.1:%s") | |
734 | """ | |
735 | _config_params = ['_testServerPort', '_dohServerPort'] | |
11c22318 RG |
736 | _checkConfigExpectedOutput = b"""No certificate provided for DoH endpoint 127.0.0.1:8480, running in DNS over HTTP mode instead of DNS over HTTPS |
737 | Configuration 'configs/dnsdist_TestDOHOverHTTP.conf' OK! | |
738 | """ | |
44947230 RG |
739 | |
740 | def testDOHSimple(self): | |
741 | """ | |
742 | DOH over HTTP: Simple query | |
743 | """ | |
744 | name = 'simple.doh-over-http.tests.powerdns.com.' | |
745 | query = dns.message.make_query(name, 'A', 'IN', use_edns=False) | |
746 | query.id = 0 | |
747 | expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096) | |
748 | response = dns.message.make_response(query) | |
749 | rrset = dns.rrset.from_text(name, | |
750 | 3600, | |
751 | dns.rdataclass.IN, | |
752 | dns.rdatatype.A, | |
753 | '127.0.0.1') | |
754 | response.answer.append(rrset) | |
755 | ||
756 | (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, useHTTPS=False) | |
757 | self.assertTrue(receivedQuery) | |
758 | self.assertTrue(receivedResponse) | |
759 | expectedQuery.id = receivedQuery.id | |
760 | self.assertEquals(expectedQuery, receivedQuery) | |
761 | self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery) | |
762 | self.assertEquals(response, receivedResponse) | |
763 | self.checkResponseNoEDNS(response, receivedResponse) | |
764 | ||
765 | def testDOHSimplePOST(self): | |
766 | """ | |
767 | DOH over HTTP: Simple POST query | |
768 | """ | |
769 | name = 'simple-post.doh-over-http.tests.powerdns.com.' | |
770 | query = dns.message.make_query(name, 'A', 'IN', use_edns=False) | |
771 | query.id = 0 | |
772 | expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096) | |
773 | expectedQuery.id = 0 | |
774 | response = dns.message.make_response(query) | |
775 | rrset = dns.rrset.from_text(name, | |
776 | 3600, | |
777 | dns.rdataclass.IN, | |
778 | dns.rdatatype.A, | |
779 | '127.0.0.1') | |
780 | response.answer.append(rrset) | |
781 | ||
782 | (receivedQuery, receivedResponse) = self.sendDOHPostQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, useHTTPS=False) | |
783 | self.assertTrue(receivedQuery) | |
784 | self.assertTrue(receivedResponse) | |
785 | receivedQuery.id = expectedQuery.id | |
786 | self.assertEquals(expectedQuery, receivedQuery) | |
787 | self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery) | |
788 | self.assertEquals(response, receivedResponse) | |
789 | self.checkResponseNoEDNS(response, receivedResponse) | |
d27309a9 RG |
790 | |
791 | class TestDOHWithCache(DNSDistDOHTest): | |
792 | ||
793 | _serverKey = 'server.key' | |
794 | _serverCert = 'server.chain' | |
795 | _serverName = 'tls.tests.dnsdist.org' | |
796 | _caCert = 'ca.pem' | |
797 | _dohServerPort = 8443 | |
798 | _dohBaseURL = ("https://%s:%d/" % (_serverName, _dohServerPort)) | |
799 | _config_template = """ | |
800 | newServer{address="127.0.0.1:%s"} | |
801 | ||
802 | addDOHLocal("127.0.0.1:%s", "%s", "%s") | |
803 | ||
804 | pc = newPacketCache(100, {maxTTL=86400, minTTL=1}) | |
805 | getPool(""):setCache(pc) | |
806 | """ | |
807 | _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey'] | |
808 | ||
809 | def testDOHCacheLargeAnswer(self): | |
810 | """ | |
811 | DOH with cache: Check that we can cache (and retrieve) large answers | |
812 | """ | |
813 | numberOfQueries = 10 | |
814 | name = 'large.doh-with-cache.tests.powerdns.com.' | |
815 | query = dns.message.make_query(name, 'A', 'IN', use_edns=False) | |
816 | query.id = 0 | |
817 | expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096) | |
818 | expectedQuery.id = 0 | |
819 | response = dns.message.make_response(query) | |
820 | # we prepare a large answer | |
821 | content = "" | |
822 | for i in range(44): | |
823 | if len(content) > 0: | |
824 | content = content + ', ' | |
825 | content = content + (str(i)*50) | |
826 | # pad up to 4096 | |
827 | content = content + 'A'*40 | |
828 | ||
829 | rrset = dns.rrset.from_text(name, | |
830 | 3600, | |
831 | dns.rdataclass.IN, | |
832 | dns.rdatatype.TXT, | |
833 | content) | |
834 | response.answer.append(rrset) | |
835 | self.assertEquals(len(response.to_wire()), 4096) | |
836 | ||
837 | # first query to fill the cache | |
838 | (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert) | |
839 | self.assertTrue(receivedQuery) | |
840 | self.assertTrue(receivedResponse) | |
841 | receivedQuery.id = expectedQuery.id | |
842 | self.assertEquals(expectedQuery, receivedQuery) | |
843 | self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery) | |
844 | self.assertEquals(response, receivedResponse) | |
845 | ||
846 | for _ in range(numberOfQueries): | |
847 | (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, caFile=self._caCert, useQueue=False) | |
848 | self.assertEquals(receivedResponse, response) |