From: Remi Gacogne Date: Wed, 29 Jan 2020 13:57:06 +0000 (+0100) Subject: dnsdist: Implement Cache-Control headers in DoH X-Git-Tag: auth-4.3.0-beta2~39^2 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=refs%2Fpull%2F8762%2Fhead;p=thirdparty%2Fpdns.git dnsdist: Implement Cache-Control headers in DoH --- diff --git a/pdns/dnsdist-lua.cc b/pdns/dnsdist-lua.cc index 79557c6f96..a2979bac0b 100644 --- a/pdns/dnsdist-lua.cc +++ b/pdns/dnsdist-lua.cc @@ -1858,6 +1858,10 @@ void setupLuaConfig(bool client, bool configCheck) } } + if (vars->count("sendCacheControlHeaders")) { + frontend->d_sendCacheControlHeaders = boost::get((*vars)["sendCacheControlHeaders"]); + } + parseTLSConfig(frontend->d_tlsConfig, "addDOHLocal", vars); } g_dohlocals.push_back(frontend); diff --git a/pdns/dnsdistdist/docs/reference/config.rst b/pdns/dnsdistdist/docs/reference/config.rst index 9aa47383f8..1ea9c843fb 100644 --- a/pdns/dnsdistdist/docs/reference/config.rst +++ b/pdns/dnsdistdist/docs/reference/config.rst @@ -107,6 +107,9 @@ Listen Sockets .. versionadded:: 1.4.0 + .. versionchanged:: 1.5.0 + ``sendCacheControlHeaders`` option added. + Listen on the specified address and TCP port for incoming DNS over HTTPS connections, presenting the specified X.509 certificate. If no certificate (or key) files are specified, listen for incoming DNS over HTTP connections instead. @@ -137,6 +140,7 @@ Listen Sockets * ``numberOfStoredSessions``: int - The maximum number of sessions kept in memory at the same time. Default is 20480. Setting this value to 0 disables stored session entirely. * ``preferServerCiphers``: bool - Whether to prefer the order of ciphers set by the server instead of the one set by the client. Default is true, meaning that the order of the server is used. * ``keyLogFile``: str - Write the TLS keys in the specified file so that an external program can decrypt TLS exchanges, in the format described in https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS/Key_Log_Format. + * ``sendCacheControlHeaders``: bool - Whether to parse the response to find the lowest TTL and set a HTTP Cache-Control header accordingly. Default is true. .. function:: addTLSLocal(address, certFile(s), keyFile(s) [, options]) diff --git a/pdns/dnsdistdist/doh.cc b/pdns/dnsdistdist/doh.cc index 9b29e650b8..6f5d1b8fe6 100644 --- a/pdns/dnsdistdist/doh.cc +++ b/pdns/dnsdistdist/doh.cc @@ -307,6 +307,16 @@ static void handleResponse(DOHFrontend& df, st_h2o_req_t* req, uint16_t statusCo } } + if (df.d_sendCacheControlHeaders && !response.empty()) { + uint32_t minTTL = getDNSPacketMinTTL(response.data(), response.size()); + if (minTTL != std::numeric_limits::max()) { + std::string cacheControlValue = "max-age=" + std::to_string(minTTL); + /* we need to duplicate the header content because h2o keeps a pointer and we will be deleted before the response has been sent */ + h2o_iovec_t ccv = h2o_strdup(&req->pool, cacheControlValue.c_str(), cacheControlValue.size()); + h2o_add_header(&req->pool, &req->res.headers, H2O_TOKEN_CACHE_CONTROL, nullptr, ccv.base, ccv.len); + } + } + req->res.content_length = response.size(); h2o_send_inline(req, response.c_str(), response.size()); } diff --git a/pdns/doh.hh b/pdns/doh.hh index ba0499f7e0..dbcfbb31ab 100644 --- a/pdns/doh.hh +++ b/pdns/doh.hh @@ -76,6 +76,7 @@ struct DOHFrontend HTTPVersionStats d_http1Stats; HTTPVersionStats d_http2Stats; + bool d_sendCacheControlHeaders{true}; time_t getTicketsKeyRotationDelay() const { diff --git a/regression-tests.dnsdist/test_DOH.py b/regression-tests.dnsdist/test_DOH.py index bb27fde286..330382c917 100644 --- a/regression-tests.dnsdist/test_DOH.py +++ b/regression-tests.dnsdist/test_DOH.py @@ -2,6 +2,8 @@ import base64 import dns import os +import re +import time import unittest import clientsubnetoption from dnsdisttests import DNSDistTest @@ -110,6 +112,21 @@ class DNSDistDOHTest(DNSDistTest): cls._response_headers = response_headers.getvalue() return (receivedQuery, message) + def getHeaderValue(self, name): + for header in self._response_headers.decode().splitlines(False): + values = header.split(':') + key = values[0] + if key.lower() == name.lower(): + return values[1].strip() + return None + + def checkHasHeader(self, name, value): + got = self.getHeaderValue(name) + self.assertEquals(got, value) + + def checkNoHeader(self, name): + self.checkHasHeader(name, None) + @classmethod def setUpClass(cls): @@ -226,8 +243,10 @@ class TestDOH(DNSDistDOHTest): self.assertTrue((self._customResponseHeader2) in self._response_headers.decode()) self.assertFalse(('UPPERCASE: VaLuE' in self._response_headers.decode())) self.assertTrue(('uppercase: VaLuE' in self._response_headers.decode())) + self.assertTrue(('cache-control: max-age=3600' in self._response_headers.decode())) self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery) self.assertEquals(response, receivedResponse) + self.checkHasHeader('cache-control', 'max-age=3600') def testDOHSimplePOST(self): """ @@ -842,7 +861,56 @@ class TestDOHWithCache(DNSDistDOHTest): self.assertEquals(expectedQuery, receivedQuery) self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery) self.assertEquals(response, receivedResponse) + self.checkHasHeader('cache-control', 'max-age=3600') for _ in range(numberOfQueries): (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, caFile=self._caCert, useQueue=False) self.assertEquals(receivedResponse, response) + self.checkHasHeader('cache-control', 'max-age=' + str(receivedResponse.answer[0].ttl)) + + time.sleep(1) + + (_, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, caFile=self._caCert, useQueue=False) + self.assertEquals(receivedResponse, response) + self.checkHasHeader('cache-control', 'max-age=' + str(receivedResponse.answer[0].ttl)) + +class TestDOHWithoutCacheControl(DNSDistDOHTest): + + _serverKey = 'server.key' + _serverCert = 'server.chain' + _serverName = 'tls.tests.dnsdist.org' + _caCert = 'ca.pem' + _dohServerPort = 8443 + _dohBaseURL = ("https://%s:%d/" % (_serverName, _dohServerPort)) + _config_template = """ + newServer{address="127.0.0.1:%s"} + + addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" }, {sendCacheControlHeaders=false}) + """ + _config_params = ['_testServerPort', '_dohServerPort', '_serverCert', '_serverKey'] + + def testDOHSimple(self): + """ + DOH without cache-control + """ + name = 'simple.doh.tests.powerdns.com.' + query = dns.message.make_query(name, 'A', 'IN', use_edns=False) + query.id = 0 + expectedQuery = dns.message.make_query(name, 'A', 'IN', use_edns=True, payload=4096) + expectedQuery.id = 0 + response = dns.message.make_response(query) + rrset = dns.rrset.from_text(name, + 3600, + dns.rdataclass.IN, + dns.rdatatype.A, + '127.0.0.1') + response.answer.append(rrset) + + (receivedQuery, receivedResponse) = self.sendDOHQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, caFile=self._caCert) + self.assertTrue(receivedQuery) + self.assertTrue(receivedResponse) + receivedQuery.id = expectedQuery.id + self.assertEquals(expectedQuery, receivedQuery) + self.checkNoHeader('cache-control') + self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery) + self.assertEquals(response, receivedResponse)