From 4494723015c8f972784dadc09188a71ead8a99cc Mon Sep 17 00:00:00 2001 From: Remi Gacogne Date: Mon, 2 Sep 2019 16:52:44 +0200 Subject: [PATCH] dnsdist: Allow accepting DoH queries over HTTP instead of HTTPS --- pdns/dnsdist-lua.cc | 15 +++- pdns/dnsdistdist/docs/reference/config.rst | 3 +- pdns/dnsdistdist/doh.cc | 26 ++++--- regression-tests.dnsdist/test_DOH.py | 88 +++++++++++++++++++--- 4 files changed, 103 insertions(+), 29 deletions(-) diff --git a/pdns/dnsdist-lua.cc b/pdns/dnsdist-lua.cc index c5de7973a..5a3a3d008 100644 --- a/pdns/dnsdist-lua.cc +++ b/pdns/dnsdist-lua.cc @@ -1678,7 +1678,7 @@ void setupLuaConfig(bool client) setSyslogFacility(facility); }); - g_lua.writeFunction("addDOHLocal", [client](const std::string& addr, boost::variant>> certFiles, boost::variant>> keyFiles, boost::optional > > > urls, boost::optional vars) { + g_lua.writeFunction("addDOHLocal", [client](const std::string& addr, boost::optional>>> certFiles, boost::optional>>> keyFiles, boost::optional > > > urls, boost::optional vars) { #ifdef HAVE_DNS_OVER_HTTPS if (client) { return; @@ -1690,11 +1690,18 @@ void setupLuaConfig(bool client) } auto frontend = std::make_shared(); - if (!loadTLSCertificateAndKeys("addDOHLocal", frontend->d_certKeyPairs, certFiles, keyFiles)) { - return; + if (certFiles && !certFiles->empty() && keyFiles && !keyFiles->empty()) { + if (!loadTLSCertificateAndKeys("addDOHLocal", frontend->d_certKeyPairs, *certFiles, *keyFiles)) { + return; + } + + frontend->d_local = ComboAddress(addr, 443); + } + else { + frontend->d_local = ComboAddress(addr, 80); + infolog("No certificate provided for DoH endpoint %s, running in DNS over HTTP mode instead of DNS over HTTPS", frontend->d_local.toStringWithPort()); } - frontend->d_local = ComboAddress(addr, 443); if (urls) { if (urls->type() == typeid(std::string)) { frontend->d_urls.push_back(boost::get(*urls)); diff --git a/pdns/dnsdistdist/docs/reference/config.rst b/pdns/dnsdistdist/docs/reference/config.rst index ff9a780cd..f09069df4 100644 --- a/pdns/dnsdistdist/docs/reference/config.rst +++ b/pdns/dnsdistdist/docs/reference/config.rst @@ -103,11 +103,12 @@ Listen Sockets higher than 0 to enable TCP Fast Open when available. Default is 0. -.. function:: addDOHLocal(address, certFile(s), keyFile(s) [, urls [, options]]) +.. function:: addDOHLocal(address, [certFile(s) [, keyFile(s) [, urls [, options]]]]) .. versionadded:: 1.4.0 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. :param str address: The IP Address with an optional port to listen on. The default port is 443. diff --git a/pdns/dnsdistdist/doh.cc b/pdns/dnsdistdist/doh.cc index 24ded5167..a4b270cd8 100644 --- a/pdns/dnsdistdist/doh.cc +++ b/pdns/dnsdistdist/doh.cc @@ -320,7 +320,7 @@ static int processDOHQuery(DOHUnit* du) if (sni != nullptr) { dq.sni = sni; } -#endif /* HAVE_H2O_SOCKET_BET_SSL_SERVER_NAME */ +#endif /* HAVE_H2O_SOCKET_GET_SSL_SERVER_NAME */ std::shared_ptr ss{nullptr}; auto result = processQuery(dq, cs, holders, ss); @@ -973,7 +973,7 @@ static void setupAcceptContext(DOHAcceptContext& ctx, DOHServerConfig& dsc, bool auto nativeCtx = ctx.get(); nativeCtx->ctx = &dsc.h2o_ctx; nativeCtx->hosts = dsc.h2o_config.hosts; - if (setupTLS) { + if (setupTLS && !dsc.df->d_certKeyPairs.empty()) { auto tlsCtx = getTLSContext(dsc.df->d_certKeyPairs, dsc.df->d_ciphers, dsc.df->d_ciphers13, @@ -1002,16 +1002,18 @@ void DOHFrontend::setup() d_dsc = std::make_shared(d_idleTimeout); - auto tlsCtx = getTLSContext(d_certKeyPairs, - d_ciphers, - d_ciphers13, - d_minTLSVersion, - d_ocspFiles, - d_dsc->accept_ctx->d_ocspResponses); - - auto accept_ctx = d_dsc->accept_ctx->get(); - accept_ctx->ssl_ctx = tlsCtx.release(); - d_dsc->accept_ctx->release(); + if (!d_certKeyPairs.empty()) { + auto tlsCtx = getTLSContext(d_certKeyPairs, + d_ciphers, + d_ciphers13, + d_minTLSVersion, + d_ocspFiles, + d_dsc->accept_ctx->d_ocspResponses); + + auto accept_ctx = d_dsc->accept_ctx->get(); + accept_ctx->ssl_ctx = tlsCtx.release(); + d_dsc->accept_ctx->release(); + } } // this is the entrypoint from dnsdist.cc diff --git a/regression-tests.dnsdist/test_DOH.py b/regression-tests.dnsdist/test_DOH.py index f833dbae9..db207f856 100644 --- a/regression-tests.dnsdist/test_DOH.py +++ b/regression-tests.dnsdist/test_DOH.py @@ -30,19 +30,21 @@ class DNSDistDOHTest(DNSDistTest): return conn @classmethod - def sendDOHQuery(cls, port, servername, baseurl, query, response=None, timeout=2.0, caFile=None, useQueue=True, rawQuery=False, rawResponse=False, customHeaders=[]): + def sendDOHQuery(cls, port, servername, baseurl, query, response=None, timeout=2.0, caFile=None, useQueue=True, rawQuery=False, rawResponse=False, customHeaders=[], useHTTPS=True): url = cls.getDOHGetURL(baseurl, query, rawQuery) conn = cls.openDOHConnection(port, caFile=caFile, timeout=timeout) response_headers = BytesIO() #conn.setopt(pycurl.VERBOSE, True) conn.setopt(pycurl.URL, url) conn.setopt(pycurl.RESOLVE, ["%s:%d:127.0.0.1" % (servername, port)]) - conn.setopt(pycurl.SSL_VERIFYPEER, 1) - conn.setopt(pycurl.SSL_VERIFYHOST, 2) + if useHTTPS: + conn.setopt(pycurl.SSL_VERIFYPEER, 1) + conn.setopt(pycurl.SSL_VERIFYHOST, 2) + if caFile: + conn.setopt(pycurl.CAINFO, caFile) + conn.setopt(pycurl.HTTPHEADER, customHeaders) conn.setopt(pycurl.HEADERFUNCTION, response_headers.write) - if caFile: - conn.setopt(pycurl.CAINFO, caFile) if response: cls._toResponderQueue.put(response, True, timeout) @@ -64,15 +66,19 @@ class DNSDistDOHTest(DNSDistTest): return (receivedQuery, message) @classmethod - def sendDOHPostQuery(cls, port, servername, baseurl, query, response=None, timeout=2.0, caFile=None, useQueue=True, rawQuery=False, rawResponse=False, customHeaders=[]): + def sendDOHPostQuery(cls, port, servername, baseurl, query, response=None, timeout=2.0, caFile=None, useQueue=True, rawQuery=False, rawResponse=False, customHeaders=[], useHTTPS=True): url = baseurl conn = cls.openDOHConnection(port, caFile=caFile, timeout=timeout) response_headers = BytesIO() #conn.setopt(pycurl.VERBOSE, True) conn.setopt(pycurl.URL, url) conn.setopt(pycurl.RESOLVE, ["%s:%d:127.0.0.1" % (servername, port)]) - conn.setopt(pycurl.SSL_VERIFYPEER, 1) - conn.setopt(pycurl.SSL_VERIFYHOST, 2) + if useHTTPS: + conn.setopt(pycurl.SSL_VERIFYPEER, 1) + conn.setopt(pycurl.SSL_VERIFYHOST, 2) + if caFile: + conn.setopt(pycurl.CAINFO, caFile) + conn.setopt(pycurl.HTTPHEADER, customHeaders) conn.setopt(pycurl.HEADERFUNCTION, response_headers.write) conn.setopt(pycurl.POST, True) @@ -82,9 +88,6 @@ class DNSDistDOHTest(DNSDistTest): conn.setopt(pycurl.POSTFIELDS, data) - if caFile: - conn.setopt(pycurl.CAINFO, caFile) - if response: cls._toResponderQueue.put(response, True, timeout) @@ -613,7 +616,6 @@ class TestDOHAddingECS(DNSDistDOHTest): _serverName = 'tls.tests.dnsdist.org' _caCert = 'ca.pem' _dohServerPort = 8443 - _serverName = 'tls.tests.dnsdist.org' _dohBaseURL = ("https://%s:%d/" % (_serverName, _dohServerPort)) _config_template = """ newServer{address="127.0.0.1:%s", useClientSubnet=true} @@ -701,3 +703,65 @@ class TestDOHAddingECS(DNSDistDOHTest): self.assertEquals(response, receivedResponse) self.checkQueryEDNSWithECS(expectedQuery, receivedQuery) self.checkResponseEDNSWithECS(response, receivedResponse) + +class TestDOHOverHTTP(DNSDistDOHTest): + + _dohServerPort = 8480 + _serverName = 'tls.tests.dnsdist.org' + _dohBaseURL = ("http://%s:%d/" % (_serverName, _dohServerPort)) + _config_template = """ + newServer{address="127.0.0.1:%s"} + addDOHLocal("127.0.0.1:%s") + """ + _config_params = ['_testServerPort', '_dohServerPort'] + + def testDOHSimple(self): + """ + DOH over HTTP: Simple query + """ + name = 'simple.doh-over-http.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) + 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, useHTTPS=False) + self.assertTrue(receivedQuery) + self.assertTrue(receivedResponse) + expectedQuery.id = receivedQuery.id + self.assertEquals(expectedQuery, receivedQuery) + self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery) + self.assertEquals(response, receivedResponse) + self.checkResponseNoEDNS(response, receivedResponse) + + def testDOHSimplePOST(self): + """ + DOH over HTTP: Simple POST query + """ + name = 'simple-post.doh-over-http.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.sendDOHPostQuery(self._dohServerPort, self._serverName, self._dohBaseURL, query, response=response, useHTTPS=False) + self.assertTrue(receivedQuery) + self.assertTrue(receivedResponse) + receivedQuery.id = expectedQuery.id + self.assertEquals(expectedQuery, receivedQuery) + self.checkQueryEDNSWithoutECS(expectedQuery, receivedQuery) + self.assertEquals(response, receivedResponse) + self.checkResponseNoEDNS(response, receivedResponse) -- 2.40.0