]>
Commit | Line | Data |
---|---|---|
1 | #!/usr/bin/env python | |
2 | ||
3 | import base64 | |
4 | import dns | |
5 | import os | |
6 | import time | |
7 | import unittest | |
8 | import clientsubnetoption | |
9 | ||
10 | from dnsdistdohtests import DNSDistDOHTest | |
11 | from dnsdisttests import DNSDistTest, pickAvailablePort | |
12 | ||
13 | import pycurl | |
14 | from io import BytesIO | |
15 | ||
16 | class DOHTests(object): | |
17 | _consoleKey = DNSDistTest.generateConsoleKey() | |
18 | _consoleKeyB64 = base64.b64encode(_consoleKey).decode('ascii') | |
19 | _serverKey = 'server.key' | |
20 | _serverCert = 'server.chain' | |
21 | _serverName = 'tls.tests.dnsdist.org' | |
22 | _caCert = 'ca.pem' | |
23 | _dohServerPort = pickAvailablePort() | |
24 | _customResponseHeader1 = 'access-control-allow-origin: *' | |
25 | _customResponseHeader2 = 'user-agent: derp' | |
26 | _dohBaseURL = ("https://%s:%d/" % (_serverName, _dohServerPort)) | |
27 | _config_template = """ | |
28 | setKey("%s") | |
29 | controlSocket("127.0.0.1:%s") | |
30 | ||
31 | newServer{address="127.0.0.1:%d"} | |
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")) | |
36 | addAction(HTTPHeaderRule("X-PowerDNS", "^[a]{5}$"), SpoofAction("2.3.4.5")) | |
37 | addAction(HTTPPathRule("/PowerDNS"), SpoofAction("3.4.5.6")) | |
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")) | |
41 | addAction("no-backend.doh.tests.powerdns.com.", PoolAction('this-pool-has-no-backend')) | |
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)) | |
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'})}) | |
65 | """ | |
66 | _config_params = ['_consoleKeyB64', '_consolePort', '_testServerPort', '_serverName', '_dohServerPort', '_dohServerPort', '_serverCert', '_serverKey', '_dohLibrary'] | |
67 | _verboseMode = True | |
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 | |
90 | self.assertEqual(expectedQuery, receivedQuery) | |
91 | self.assertTrue((self._customResponseHeader1) in self._response_headers.decode()) | |
92 | self.assertTrue((self._customResponseHeader2) in self._response_headers.decode()) | |
93 | self.assertFalse(('UPPERCASE: VaLuE' in self._response_headers.decode())) | |
94 | self.assertTrue(('uppercase: VaLuE' in self._response_headers.decode())) | |
95 | self.assertTrue(('cache-control: max-age=3600' in self._response_headers.decode())) | |
96 | self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery) | |
97 | self.assertEqual(response, receivedResponse) | |
98 | self.checkHasHeader('cache-control', 'max-age=3600') | |
99 | ||
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 | |
121 | self.assertEqual(expectedQuery, receivedQuery) | |
122 | self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery) | |
123 | self.assertEqual(response, receivedResponse) | |
124 | # just to be sure the ID _is_ checked | |
125 | self.assertEqual(response.id, receivedResponse.id) | |
126 | ||
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 | |
148 | self.assertEqual(expectedQuery, receivedQuery) | |
149 | self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery) | |
150 | self.assertEqual(response, receivedResponse) | |
151 | ||
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 | |
171 | self.assertEqual(query, receivedQuery) | |
172 | self.assertEqual(response, receivedResponse) | |
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 | |
198 | self.assertEqual(query, receivedQuery) | |
199 | self.assertEqual(response, receivedResponse) | |
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) | |
210 | self.assertEqual(receivedResponse, None) | |
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 | |
219 | query.flags &= ~dns.flags.RD | |
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) | |
224 | self.assertEqual(receivedResponse, expectedResponse) | |
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) | |
243 | self.assertEqual(receivedResponse, expectedResponse) | |
244 | ||
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 | ||
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 | ||
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 | ||
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 | ||
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') | |
403 | httpConnections = self.getHTTPCounter('connects') | |
404 | http1 = self.getHTTPCounter('http/1.1') | |
405 | http2 = self.getHTTPCounter('http/2') | |
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) | |
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') | |
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) | |
427 | ||
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 | ||
440 | def testDOHInvalid(self): | |
441 | """ | |
442 | DOH: Invalid DNS query | |
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) | |
451 | self.assertEqual(receivedResponse, None) | |
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 | |
469 | self.assertEqual(expectedQuery, receivedQuery) | |
470 | self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery) | |
471 | self.assertEqual(response, receivedResponse) | |
472 | ||
473 | def testDOHInvalidHeaderName(self): | |
474 | """ | |
475 | DOH: Invalid HTTP header name query | |
476 | """ | |
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) | |
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) | |
517 | self.assertEqual(rcode, 403) | |
518 | ||
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) | |
526 | self.assertEqual(receivedResponse, None) | |
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 | |
544 | self.assertEqual(expectedQuery, receivedQuery) | |
545 | self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery) | |
546 | self.assertEqual(response, receivedResponse) | |
547 | ||
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']) | |
566 | self.assertEqual(receivedResponse, expectedResponse) | |
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 | |
584 | self.assertEqual(expectedQuery, receivedQuery) | |
585 | self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery) | |
586 | self.assertEqual(response, receivedResponse) | |
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) | |
606 | self.assertEqual(receivedResponse, expectedResponse) | |
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 | |
624 | self.assertEqual(expectedQuery, receivedQuery) | |
625 | self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery) | |
626 | self.assertEqual(response, receivedResponse) | |
627 | ||
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) | |
631 | self.assertEqual(receivedResponse, b'there is no endpoint configured for this path') | |
632 | self.assertEqual(self._rcode, 404) | |
633 | ||
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) | |
652 | self.assertEqual(receivedResponse, expectedResponse) | |
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 | |
670 | self.assertEqual(expectedQuery, receivedQuery) | |
671 | self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery) | |
672 | self.assertEqual(response, receivedResponse) | |
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) | |
684 | self.assertEqual(receivedResponse, b'Plaintext answer') | |
685 | self.assertEqual(self._rcode, 200) | |
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) | |
698 | self.assertEqual(self._rcode, 307) | |
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) | |
711 | self.assertEqual(receivedResponse, b'It works!') | |
712 | self.assertEqual(self._rcode, 200) | |
713 | self.assertTrue('content-type: text/plain' in self._response_headers.decode()) | |
714 | ||
715 | def testHTTPEarlyResponse(self): | |
716 | """ | |
717 | DOH: HTTP Early Response | |
718 | """ | |
719 | response_headers = BytesIO() | |
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) | |
727 | conn.setopt(pycurl.HEADERFUNCTION, response_headers.write) | |
728 | data = conn.perform_rb() | |
729 | rcode = conn.getinfo(pycurl.RESPONSE_CODE) | |
730 | headers = response_headers.getvalue().decode() | |
731 | ||
732 | self.assertEqual(rcode, 418) | |
733 | self.assertEqual(data, b'C0FFEE') | |
734 | self.assertIn('foo: bar', headers) | |
735 | self.assertNotIn(self._customResponseHeader2, headers) | |
736 | ||
737 | response_headers = BytesIO() | |
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) | |
744 | conn.setopt(pycurl.HEADERFUNCTION, response_headers.write) | |
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) | |
751 | headers = response_headers.getvalue().decode() | |
752 | self.assertEqual(rcode, 418) | |
753 | self.assertEqual(data, b'C0FFEE') | |
754 | self.assertIn('foo: bar', headers) | |
755 | self.assertNotIn(self._customResponseHeader2, headers) | |
756 | ||
757 | class TestDoHNGHTTP2(DOHTests, DNSDistDOHTest): | |
758 | _dohLibrary = 'nghttp2' | |
759 | ||
760 | class TestDoHH2O(DOHTests, DNSDistDOHTest): | |
761 | _dohLibrary = 'h2o' | |
762 | ||
763 | class DOHSubPathsTests(object): | |
764 | _serverKey = 'server.key' | |
765 | _serverCert = 'server.chain' | |
766 | _serverName = 'tls.tests.dnsdist.org' | |
767 | _caCert = 'ca.pem' | |
768 | _dohServerPort = pickAvailablePort() | |
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 | ||
775 | addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/PowerDNS" }, {exactPathMatching=false, library='%s'}) | |
776 | """ | |
777 | _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey', '_dohLibrary'] | |
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) | |
797 | self.assertEqual(receivedResponse, expectedResponse) | |
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) | |
813 | self.assertIn(receivedResponse, [b'there is no endpoint configured for this path', b'not found']) | |
814 | self.assertEqual(self._rcode, 404) | |
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) | |
818 | self.assertEqual(receivedResponse, expectedResponse) | |
819 | ||
820 | class TestDoHSubPathsNGHTTP2(DOHSubPathsTests, DNSDistDOHTest): | |
821 | _dohLibrary = 'nghttp2' | |
822 | ||
823 | class TestDoHSubPathsH2O(DOHSubPathsTests, DNSDistDOHTest): | |
824 | _dohLibrary = 'h2o' | |
825 | ||
826 | class DOHAddingECSTests(object): | |
827 | ||
828 | _serverKey = 'server.key' | |
829 | _serverCert = 'server.chain' | |
830 | _serverName = 'tls.tests.dnsdist.org' | |
831 | _caCert = 'ca.pem' | |
832 | _dohServerPort = pickAvailablePort() | |
833 | _dohBaseURL = ("https://%s:%d/" % (_serverName, _dohServerPort)) | |
834 | _config_template = """ | |
835 | newServer{address="127.0.0.1:%s", useClientSubnet=true} | |
836 | addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" }, {library='%s'}) | |
837 | setECSOverride(true) | |
838 | """ | |
839 | _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey', '_dohLibrary'] | |
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 | |
862 | self.assertEqual(expectedQuery, receivedQuery) | |
863 | self.checkQueryEDNSWithECS(expectedQuery, receivedQuery) | |
864 | self.assertEqual(response, receivedResponse) | |
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 | |
888 | self.assertEqual(expectedQuery, receivedQuery) | |
889 | self.assertEqual(response, receivedResponse) | |
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 | |
916 | self.assertEqual(expectedQuery, receivedQuery) | |
917 | self.assertEqual(response, receivedResponse) | |
918 | self.checkQueryEDNSWithECS(expectedQuery, receivedQuery) | |
919 | self.checkResponseEDNSWithECS(response, receivedResponse) | |
920 | ||
921 | class TestDoHAddingECSNGHTTP2(DOHAddingECSTests, DNSDistDOHTest): | |
922 | _dohLibrary = 'nghttp2' | |
923 | ||
924 | class TestDoHAddingECSH2O(DOHAddingECSTests, DNSDistDOHTest): | |
925 | _dohLibrary = 'h2o' | |
926 | ||
927 | class DOHOverHTTP(object): | |
928 | _dohServerPort = pickAvailablePort() | |
929 | _serverName = 'tls.tests.dnsdist.org' | |
930 | _dohBaseURL = ("http://%s:%d/dns-query" % (_serverName, _dohServerPort)) | |
931 | _config_template = """ | |
932 | newServer{address="127.0.0.1:%s"} | |
933 | addDOHLocal("127.0.0.1:%s", nil, nil, '/dns-query', {library='%s'}) | |
934 | """ | |
935 | _config_params = ['_testServerPort', '_dohServerPort', '_dohLibrary'] | |
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 | |
957 | self.assertEqual(expectedQuery, receivedQuery) | |
958 | self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery) | |
959 | self.assertEqual(response, receivedResponse) | |
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 | |
983 | self.assertEqual(expectedQuery, receivedQuery) | |
984 | self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery) | |
985 | self.assertEqual(response, receivedResponse) | |
986 | self.checkResponseNoEDNS(response, receivedResponse) | |
987 | ||
988 | class 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 | |
991 | Configuration 'configs/dnsdist_TestDOHOverHTTPNGHTTP2.conf' OK! | |
992 | """ % (DOHOverHTTP._dohServerPort) | |
993 | ||
994 | class 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 | |
997 | Configuration 'configs/dnsdist_TestDOHOverHTTPH2O.conf' OK! | |
998 | """ % (DOHOverHTTP._dohServerPort) | |
999 | ||
1000 | class DOHWithCache(object): | |
1001 | ||
1002 | _serverKey = 'server.key' | |
1003 | _serverCert = 'server.chain' | |
1004 | _serverName = 'tls.tests.dnsdist.org' | |
1005 | _caCert = 'ca.pem' | |
1006 | _dohServerPort = pickAvailablePort() | |
1007 | _dohBaseURL = ("https://%s:%d/dns-query" % (_serverName, _dohServerPort)) | |
1008 | _config_template = """ | |
1009 | newServer{address="127.0.0.1:%s"} | |
1010 | ||
1011 | addDOHLocal("127.0.0.1:%s", "%s", "%s", '/dns-query', {library='%s'}) | |
1012 | ||
1013 | pc = newPacketCache(100, {maxTTL=86400, minTTL=1}) | |
1014 | getPool(""):setCache(pc) | |
1015 | """ | |
1016 | _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey', '_dohLibrary'] | |
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) | |
1044 | self.assertEqual(len(response.to_wire()), 4096) | |
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 | |
1051 | self.assertEqual(expectedQuery, receivedQuery) | |
1052 | self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery) | |
1053 | self.assertEqual(response, receivedResponse) | |
1054 | self.checkHasHeader('cache-control', 'max-age=3600') | |
1055 | ||
1056 | for _ in range(numberOfQueries): | |
1057 | (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, caFile=self._caCert, useQueue=False) | |
1058 | self.assertEqual(receivedResponse, response) | |
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) | |
1064 | self.assertEqual(receivedResponse, response) | |
1065 | self.checkHasHeader('cache-control', 'max-age=' + str(receivedResponse.answer[0].ttl)) | |
1066 | ||
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 | ||
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') | |
1133 | query.id = 42 | |
1134 | expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096) | |
1135 | expectedQuery.id = 42 | |
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) | |
1150 | # first query, received by the responder over UDP | |
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 | ||
1160 | # check the second query, received by the responder over TCP | |
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 | ||
1209 | class TestDOHWithCacheNGHTTP2(DOHWithCache, DNSDistDOHTest): | |
1210 | _dohLibrary = 'nghttp2' | |
1211 | _verboseMode = True | |
1212 | ||
1213 | class TestDOHWithCacheH2O(DOHWithCache, DNSDistDOHTest): | |
1214 | _dohLibrary = 'h2o' | |
1215 | ||
1216 | class DOHWithoutCacheControl(object): | |
1217 | ||
1218 | _serverKey = 'server.key' | |
1219 | _serverCert = 'server.chain' | |
1220 | _serverName = 'tls.tests.dnsdist.org' | |
1221 | _caCert = 'ca.pem' | |
1222 | _dohServerPort = pickAvailablePort() | |
1223 | _dohBaseURL = ("https://%s:%d/" % (_serverName, _dohServerPort)) | |
1224 | _config_template = """ | |
1225 | newServer{address="127.0.0.1:%s"} | |
1226 | ||
1227 | addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" }, {sendCacheControlHeaders=false, library='%s'}) | |
1228 | """ | |
1229 | _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey', '_dohLibrary'] | |
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 | |
1252 | self.assertEqual(expectedQuery, receivedQuery) | |
1253 | self.checkNoHeader('cache-control') | |
1254 | self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery) | |
1255 | self.assertEqual(response, receivedResponse) | |
1256 | ||
1257 | class TestDOHWithoutCacheControlNGHTTP2(DOHWithoutCacheControl, DNSDistDOHTest): | |
1258 | _dohLibrary = 'nghttp2' | |
1259 | ||
1260 | class TestDOHWithoutCacheControlH2O(DOHWithoutCacheControl, DNSDistDOHTest): | |
1261 | _dohLibrary = 'h2o' | |
1262 | ||
1263 | class DOHFFI(object): | |
1264 | _serverKey = 'server.key' | |
1265 | _serverCert = 'server.chain' | |
1266 | _serverName = 'tls.tests.dnsdist.org' | |
1267 | _caCert = 'ca.pem' | |
1268 | _dohServerPort = pickAvailablePort() | |
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 | ||
1275 | addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" }, {customResponseHeaders={["access-control-allow-origin"]="*",["user-agent"]="derp",["UPPERCASE"]="VaLuE"}, keepIncomingHeaders=true, library='%s'}) | |
1276 | ||
1277 | local ffi = require("ffi") | |
1278 | ||
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 | |
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) | |
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 | |
1299 | local response = 'It works!' | |
1300 | ffi.C.dnsdist_ffi_dnsquestion_set_http_response(dq, 200, response, #response, 'text/plain') | |
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 | """ | |
1308 | _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey', '_dohLibrary', '_serverName', '_dohServerPort'] | |
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) | |
1320 | self.assertEqual(receivedResponse, b'It works!') | |
1321 | self.assertEqual(self._rcode, 200) | |
1322 | self.assertTrue('content-type: text/plain' in self._response_headers.decode()) | |
1323 | ||
1324 | class TestDOHFFINGHTTP2(DOHFFI, DNSDistDOHTest): | |
1325 | _dohLibrary = 'nghttp2' | |
1326 | ||
1327 | class TestDOHFFIH2O(DOHFFI, DNSDistDOHTest): | |
1328 | _dohLibrary = 'h2o' | |
1329 | ||
1330 | class DOHForwardedFor(object): | |
1331 | _serverKey = 'server.key' | |
1332 | _serverCert = 'server.chain' | |
1333 | _serverName = 'tls.tests.dnsdist.org' | |
1334 | _caCert = 'ca.pem' | |
1335 | _dohServerPort = pickAvailablePort() | |
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') | |
1341 | addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" }, {trustForwardedForHeader=true, library='%s'}) | |
1342 | -- Set a maximum number of TCP connections per client, to exercise | |
1343 | -- that code along with X-Forwarded-For support | |
1344 | setMaxTCPConnectionsPerClient(2) | |
1345 | """ | |
1346 | _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey', '_dohLibrary'] | |
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 | |
1369 | self.assertEqual(expectedQuery, receivedQuery) | |
1370 | self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery) | |
1371 | self.assertEqual(response, receivedResponse) | |
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 | ||
1392 | self.assertEqual(self._rcode, 403) | |
1393 | self.assertEqual(receivedResponse, b'DoH query not allowed because of ACL') | |
1394 | ||
1395 | class TestDOHForwardedForNGHTTP2(DOHForwardedFor, DNSDistDOHTest): | |
1396 | _dohLibrary = 'nghttp2' | |
1397 | ||
1398 | class TestDOHForwardedForH2O(DOHForwardedFor, DNSDistDOHTest): | |
1399 | _dohLibrary = 'h2o' | |
1400 | ||
1401 | class DOHForwardedForNoTrusted(object): | |
1402 | ||
1403 | _serverKey = 'server.key' | |
1404 | _serverCert = 'server.chain' | |
1405 | _serverName = 'tls.tests.dnsdist.org' | |
1406 | _caCert = 'ca.pem' | |
1407 | _dohServerPort = pickAvailablePort() | |
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') | |
1413 | addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" }, {earlyACLDrop=true, library='%s'}) | |
1414 | """ | |
1415 | _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey', '_dohLibrary'] | |
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 | ||
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 | |
1441 | ||
1442 | self.assertTrue(dropped) | |
1443 | ||
1444 | class TestDOHForwardedForNoTrustedNGHTTP2(DOHForwardedForNoTrusted, DNSDistDOHTest): | |
1445 | _dohLibrary = 'nghttp2' | |
1446 | ||
1447 | class TestDOHForwardedForNoTrustedH2O(DOHForwardedForNoTrusted, DNSDistDOHTest): | |
1448 | _dohLibrary = 'h2o' | |
1449 | ||
1450 | class DOHFrontendLimits(object): | |
1451 | ||
1452 | # this test suite uses a different responder port | |
1453 | # because it uses a different health check configuration | |
1454 | _testServerPort = pickAvailablePort() | |
1455 | _answerUnexpected = True | |
1456 | ||
1457 | _serverKey = 'server.key' | |
1458 | _serverCert = 'server.chain' | |
1459 | _serverName = 'tls.tests.dnsdist.org' | |
1460 | _caCert = 'ca.pem' | |
1461 | _dohServerPort = pickAvailablePort() | |
1462 | _dohBaseURL = ("https://%s:%d/" % (_serverName, _dohServerPort)) | |
1463 | _skipListeningOnCL = True | |
1464 | _maxTCPConnsPerDOHFrontend = 5 | |
1465 | _config_template = """ | |
1466 | newServer{address="127.0.0.1:%s"} | |
1467 | addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" }, { maxConcurrentTCPConnections=%d, library='%s' }) | |
1468 | """ | |
1469 | _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey', '_maxTCPConnsPerDOHFrontend', '_dohLibrary'] | |
1470 | _alternateListeningAddr = '127.0.0.1' | |
1471 | _alternateListeningPort = _dohServerPort | |
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: | |
1483 | alpn = [] | |
1484 | if self._dohLibrary != 'h2o': | |
1485 | alpn.append('h2') | |
1486 | conns.append(self.openTLSConnection(self._dohServerPort, self._serverName, self._caCert, alpn=alpn)) | |
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) | |
1518 | ||
1519 | class TestDOHFrontendLimitsNGHTTP2(DOHFrontendLimits, DNSDistDOHTest): | |
1520 | _dohLibrary = 'nghttp2' | |
1521 | ||
1522 | class TestDOHFrontendLimitsH2O(DOHFrontendLimits, DNSDistDOHTest): | |
1523 | _dohLibrary = 'h2o' | |
1524 | ||
1525 | class Protocols(object): | |
1526 | _serverKey = 'server.key' | |
1527 | _serverCert = 'server.chain' | |
1528 | _serverName = 'tls.tests.dnsdist.org' | |
1529 | _caCert = 'ca.pem' | |
1530 | _dohServerPort = pickAvailablePort() | |
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"} | |
1544 | addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" }, {library='%s'}) | |
1545 | """ | |
1546 | _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey', '_dohLibrary'] | |
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) | |
1565 | ||
1566 | class TestProtocolsNGHTTP2(Protocols, DNSDistDOHTest): | |
1567 | _dohLibrary = 'nghttp2' | |
1568 | ||
1569 | class TestProtocolsH2O(Protocols, DNSDistDOHTest): | |
1570 | _dohLibrary = 'h2o' | |
1571 | ||
1572 | class DOHWithPKCS12Cert(object): | |
1573 | _serverCert = 'server.p12' | |
1574 | _pkcs12Password = 'passw0rd' | |
1575 | _serverName = 'tls.tests.dnsdist.org' | |
1576 | _caCert = 'ca.pem' | |
1577 | _dohServerPort = pickAvailablePort() | |
1578 | _dohBaseURL = ("https://%s:%d/" % (_serverName, _dohServerPort)) | |
1579 | _config_template = """ | |
1580 | newServer{address="127.0.0.1:%s"} | |
1581 | cert=newTLSCertificate("%s", {password="%s"}) | |
1582 | addDOHLocal("127.0.0.1:%s", cert, "", { "/" }, {library='%s'}) | |
1583 | """ | |
1584 | _config_params = ['_testServerPort', '_serverCert', '_pkcs12Password', '_dohServerPort', '_dohLibrary'] | |
1585 | ||
1586 | def testPKCS12DOH(self): | |
1587 | """ | |
1588 | DoH: Test Simple DOH Query with a password protected PKCS12 file configured | |
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) | |
1608 | ||
1609 | class TestDOHWithPKCS12CertNGHTTP2(DOHWithPKCS12Cert, DNSDistDOHTest): | |
1610 | _dohLibrary = 'nghttp2' | |
1611 | ||
1612 | class TestDOHWithPKCS12CertH2O(DOHWithPKCS12Cert, DNSDistDOHTest): | |
1613 | _dohLibrary = 'h2o' | |
1614 | ||
1615 | class DOHForwardedToTCPOnly(object): | |
1616 | _serverKey = 'server.key' | |
1617 | _serverCert = 'server.chain' | |
1618 | _serverName = 'tls.tests.dnsdist.org' | |
1619 | _caCert = 'ca.pem' | |
1620 | _dohServerPort = pickAvailablePort() | |
1621 | _dohBaseURL = ("https://%s:%d/" % (_serverName, _dohServerPort)) | |
1622 | _config_template = """ | |
1623 | newServer{address="127.0.0.1:%s", tcpOnly=true} | |
1624 | addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" }, {library='%s'}) | |
1625 | """ | |
1626 | _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey', '_dohLibrary'] | |
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) | |
1649 | ||
1650 | class TestDOHForwardedToTCPOnlyNGHTTP2(DOHForwardedToTCPOnly, DNSDistDOHTest): | |
1651 | _dohLibrary = 'nghttp2' | |
1652 | ||
1653 | class TestDOHForwardedToTCPOnlyH2O(DOHForwardedToTCPOnly, DNSDistDOHTest): | |
1654 | _dohLibrary = 'h2o' | |
1655 | ||
1656 | class DOHLimits(object): | |
1657 | _serverName = 'tls.tests.dnsdist.org' | |
1658 | _caCert = 'ca.pem' | |
1659 | _dohServerPort = pickAvailablePort() | |
1660 | _dohBaseURL = ("https://%s:%d/" % (_serverName, _dohServerPort)) | |
1661 | _serverKey = 'server.key' | |
1662 | _serverCert = 'server.chain' | |
1663 | _maxTCPConnsPerClient = 3 | |
1664 | _config_template = """ | |
1665 | newServer{address="127.0.0.1:%d"} | |
1666 | addDOHLocal("127.0.0.1:%d", "%s", "%s", { "/" }, {library='%s'}) | |
1667 | setMaxTCPConnectionsPerClient(%d) | |
1668 | """ | |
1669 | _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey', '_dohLibrary', '_maxTCPConnsPerClient'] | |
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) | |
1709 | ||
1710 | class TestDOHLimitsNGHTTP2(DOHLimits, DNSDistDOHTest): | |
1711 | _dohLibrary = 'nghttp2' | |
1712 | ||
1713 | class TestDOHLimitsH2O(DOHLimits, DNSDistDOHTest): | |
1714 | _dohLibrary = 'h2o' |