]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
dnsdist: Add support for early DoH HTTP responses
authorRemi Gacogne <remi.gacogne@powerdns.com>
Wed, 14 Aug 2019 17:15:08 +0000 (19:15 +0200)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Thu, 29 Aug 2019 13:44:22 +0000 (15:44 +0200)
pdns/dnsdist-lua-bindings.cc
pdns/dnsdist-lua.cc
pdns/dnsdistdist/docs/reference/config.rst
pdns/dnsdistdist/docs/rules-actions.rst
pdns/dnsdistdist/doh.cc
pdns/doh.hh
regression-tests.dnsdist/test_DOH.py

index 9ad4b70d370c2625d530f5643f6c8a1ecd176e51..a8c1f931d3f307cec0b5cc5015265835c66b8475 100644 (file)
@@ -369,4 +369,7 @@ void setupLuaBindings(bool client)
     return values;
   });
 
+  g_lua.writeFunction("newDOHResponseMapEntry", [](const std::string& regex, uint16_t status, const std::string& content) {
+    return std::make_shared<DOHResponseMapEntry>(regex, status, content);
+  });
 }
index a1aba5321f40f8dd06dc621644153d1c9c9fb0ce..c5de7973ac142bd863d58d5680f4700c9982c45e 100644 (file)
@@ -1812,8 +1812,11 @@ void setupLuaConfig(bool client)
 #endif
       });
 
-    g_lua.writeFunction("getDOHFrontend", [](size_t index) {
+    g_lua.writeFunction("getDOHFrontend", [client](size_t index) {
         std::shared_ptr<DOHFrontend> result = nullptr;
+        if (client) {
+          return result;
+        }
 #ifdef HAVE_DNS_OVER_HTTPS
         setLuaNoSideEffect();
         try {
@@ -1841,6 +1844,19 @@ void setupLuaConfig(bool client)
         }
       });
 
+    g_lua.registerFunction<void(std::shared_ptr<DOHFrontend>::*)(const std::map<int, std::shared_ptr<DOHResponseMapEntry>>&)>("setResponsesMap", [](std::shared_ptr<DOHFrontend> frontend, const std::map<int, std::shared_ptr<DOHResponseMapEntry>>& map) {
+        if (frontend != nullptr) {
+          std::vector<std::shared_ptr<DOHResponseMapEntry>> newMap;
+          newMap.reserve(map.size());
+
+          for (const auto& entry : map) {
+            newMap.push_back(entry.second);
+          }
+
+          frontend->d_responsesMap = std::move(newMap);
+        }
+      });
+
   g_lua.writeFunction("addTLSLocal", [client](const std::string& addr, boost::variant<std::string, std::vector<std::pair<int,std::string>>> certFiles, boost::variant<std::string, std::vector<std::pair<int,std::string>>> keyFiles, boost::optional<localbind_t> vars) {
 #ifdef HAVE_DNS_OVER_TLS
         if (client)
index b2752c653ea89431d1a05c50d03decd22500fa8c..a202ea01a52730d578cb927139cca6320d2624f4 100644 (file)
@@ -1145,6 +1145,24 @@ DOHFrontend
 
      Reload the current TLS certificate and key pairs.
 
+  .. method:: DOHFrontend:setResponsesMap(rules)
+
+     Set a list of HTTP response rules allowing to intercept HTTP queries very early, before the DNS payload has been processed, and send custom responses including error pages, redirects and static content.
+
+     :param list of DOHResponseMapEntry objects rules: A list of DOHResponseMapEntry objects, obtained with :func:`newDOHResponseMapEntry`.
+
+
+.. function:: newDOHResponseMapEntry(regex, status, content) -> DOHResponseMapEntry
+
+  .. versionadded:: 1.4.0
+
+  Return a DOHResponseMapEntry that can be used with :meth:`DOHFrontend.setResponsesMap`. Every query whose path matches the regular expression supplied in ``regex`` will be immediately answered with a HTTP response.
+  The status of the HTTP response will be the one supplied by ``status``, and the content set to the one supplied by ``content``, except if the status is a redirection (3xx) in which case the content is expected to be the URL to redirect to.
+
+  :param str regex: A regular expression to match the path against.
+  :param int status: The HTTP code to answer with.
+  :param str content: The content of the HTTP response, or a URL if the status is a redirection (3xx).
+
 TLSContext
 ~~~~~~~~~~
 
index d2a09dc811697680ba1c59400ed153e5142fc04c..239f8b2a3f866e5ab72caa37348624837742a0d1 100644 (file)
@@ -587,6 +587,7 @@ These ``DNSRule``\ s be one of the following items:
   .. versionadded:: 1.4.0
 
   Matches DNS over HTTPS queries with a HTTP path matching the regular expression supplied in ``regex``. For example, if the query has been sent to the https://192.0.2.1:443/PowerDNS?dns=... URL, the path would be '/PowerDNS'.
+  Only valid DNS over HTTPS queries are matched. If you want to match all HTTP queries, see :meth:`DOHFrontend.setResponsesMap` instead.
 
   :param str regex: The regex to match on
 
@@ -594,6 +595,7 @@ These ``DNSRule``\ s be one of the following items:
   .. versionadded:: 1.4.0
 
   Matches DNS over HTTPS queries with a HTTP path of ``path``. For example, if the query has been sent to the https://192.0.2.1:443/PowerDNS?dns=... URL, the path would be '/PowerDNS'.
+  Only valid DNS over HTTPS queries are matched. If you want to match all HTTP queries, see :meth:`DOHFrontend.setResponsesMap` instead.
 
   :param str path: The exact HTTP path to match on
 
index d918d531ccdef062dedd54c3032d1ff1e3a2d1fe..6c86158ba9a30c3cfc184e9ed240a8bf415c3af4 100644 (file)
@@ -149,7 +149,111 @@ static void on_socketclose(void *data)
   ctx->release();
 }
 
-/* this duplicates way too much from the UDP handler. Sorry.
+
+static const std::string& getReasonFromStatusCode(uint16_t statusCode)
+{
+  /* no need to care too much about this, HTTP/2 has no 'reason' anyway */
+  static const std::unordered_map<uint16_t, std::string> reasons = {
+    { 200, "OK" },
+    { 301, "Moved Permanently" },
+    { 302, "Found" },
+    { 303, "See Other" },
+    { 304, "Not Modified" },
+    { 305, "Use Proxy" },
+    { 306, "Switch Proxy" },
+    { 307, "Temporary Redirect" },
+    { 308, "Permanent Redirect" },
+    { 400, "Bad Request" },
+    { 401, "Unauthorized" },
+    { 402, "Payment Required" },
+    { 403, "Forbidden" },
+    { 404, "Not Found" },
+    { 405, "Method Not Allowed" },
+    { 406, "Not Acceptable" },
+    { 407, "Proxy Authentication Required" },
+    { 408, "Request Timeout" },
+    { 409, "Conflict" },
+    { 410, "Gone" },
+    { 411, "Length Required" },
+    { 412, "Precondition Failed" },
+    { 413, "Payload Too Large" },
+    { 414, "URI Too Long" },
+    { 415, "Unsupported Media Type" },
+    { 416, "Range Not Satisfiable" },
+    { 417, "Expectation Failed" },
+    { 418, "I'm a teapot" },
+    { 451, "Unavailable For Legal Reasons" },
+    { 500, "Internal Server Error" },
+    { 501, "Not Implemented" },
+    { 502, "Bad Gateway" },
+    { 503, "Service Unavailable" },
+    { 504, "Gateway Timeout" },
+    { 505, "HTTP Version Not Supported" }
+  };
+  static const std::string unknown = "Unknown";
+
+  const auto it = reasons.find(statusCode);
+  if (it == reasons.end()) {
+    return unknown;
+  }
+  else {
+    return it->second;
+  }
+}
+
+static void handleResponse(DOHFrontend& df, st_h2o_req_t* req, uint16_t statusCode, const std::string& response, const std::string& contentType)
+{
+  if (statusCode == 200) {
+    ++df.d_validresponses;
+    req->res.status = 200;
+
+    if (contentType.empty()) {
+      h2o_add_header(&req->pool, &req->res.headers, H2O_TOKEN_CONTENT_TYPE, nullptr, H2O_STRLIT("application/dns-message"));
+    }
+    else {
+      /* 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 ct = h2o_strdup(&req->pool, contentType.c_str(), contentType.size());
+      h2o_add_header(&req->pool, &req->res.headers, H2O_TOKEN_CONTENT_TYPE, nullptr, ct.base, ct.len);
+    }
+
+    req->res.content_length = response.size();
+    h2o_send_inline(req, response.c_str(), response.size());
+  }
+  else if (statusCode >= 300 && statusCode < 400) {
+    /* in that case the response is actually a URL */
+    /* we need to duplicate the URL because h2o uses it for the location header, keeping a pointer, and we will be deleted before the response has been sent */
+    h2o_iovec_t url = h2o_strdup(&req->pool, response.c_str(), response.size());
+    h2o_send_redirect(req, statusCode, getReasonFromStatusCode(statusCode).c_str(), url.base, url.len);
+    ++df.d_redirectresponses;
+  }
+  else {
+    if (!response.empty()) {
+      h2o_send_error_generic(req, statusCode, getReasonFromStatusCode(statusCode).c_str(), response.c_str(), 0);
+    }
+    else {
+      switch(statusCode) {
+      case 400:
+        h2o_send_error_400(req, getReasonFromStatusCode(statusCode).c_str(), "invalid DNS query" , 0);
+        break;
+      case 403:
+        h2o_send_error_403(req, getReasonFromStatusCode(statusCode).c_str(), "dns query not allowed", 0);
+        break;
+      case 502:
+        h2o_send_error_502(req, getReasonFromStatusCode(statusCode).c_str(), "no downstream server available", 0);
+        break;
+      case 500:
+        /* fall-through */
+      default:
+        h2o_send_error_500(req, getReasonFromStatusCode(statusCode).c_str(), "Internal Server Error", 0);
+        break;
+      }
+    }
+
+    ++df.d_errorresponses;
+  }
+}
+
+/*
    this function calls 'return -1' to drop a query without sending it
    caller should make sure HTTPS thread hears of that
 */
@@ -470,6 +574,13 @@ try
 
   string path(req->path.base, req->path.len);
 
+  for (const auto& entry : dsc->df->d_responsesMap) {
+    if (entry->matches(path)) {
+      handleResponse(*dsc->df, req, entry->getStatusCode(), entry->getContent(), std::string());
+      return 0;
+    }
+  }
+
   if (h2o_memis(req->method.base, req->method.len, H2O_STRLIT("POST"))) {
     ++dsc->df->d_postqueries;
     if(req->version >= 0x0200)
@@ -712,58 +823,6 @@ void dnsdistclient(int qsock, int rsock)
   }
 }
 
-static const std::string& getReasonFromStatusCode(uint16_t statusCode)
-{
-  /* no need to care too much about this, HTTP/2 has no 'reason' anyway */
-  static const std::unordered_map<uint16_t, std::string> reasons = {
-    { 200, "OK" },
-    { 301, "Moved Permanently" },
-    { 302, "Found" },
-    { 303, "See Other" },
-    { 304, "Not Modified" },
-    { 305, "Use Proxy" },
-    { 306, "Switch Proxy" },
-    { 307, "Temporary Redirect" },
-    { 308, "Permanent Redirect" },
-    { 400, "Bad Request" },
-    { 401, "Unauthorized" },
-    { 402, "Payment Required" },
-    { 403, "Forbidden" },
-    { 404, "Not Found" },
-    { 405, "Method Not Allowed" },
-    { 406, "Not Acceptable" },
-    { 407, "Proxy Authentication Required" },
-    { 408, "Request Timeout" },
-    { 409, "Conflict" },
-    { 410, "Gone" },
-    { 411, "Length Required" },
-    { 412, "Precondition Failed" },
-    { 413, "Payload Too Large" },
-    { 414, "URI Too Long" },
-    { 415, "Unsupported Media Type" },
-    { 416, "Range Not Satisfiable" },
-    { 417, "Expectation Failed" },
-    { 418, "I'm a teapot" },
-    { 451, "Unavailable For Legal Reasons" },
-    { 500, "Internal Server Error" },
-    { 501, "Not Implemented" },
-    { 502, "Bad Gateway" },
-    { 503, "Service Unavailable" },
-    { 504, "Gateway Timeout" },
-    { 505, "HTTP Version Not Supported" }
-  };
-  static const std::string unknown = "Unknown";
-
-  const auto it = reasons.find(statusCode);
-  if (it == reasons.end()) {
-    return unknown;
-  }
-  else {
-    return it->second;
-  }
-}
-
-
 // called if h2o finds that dnsdist gave us an answer
 static void on_dnsdist(h2o_socket_t *listener, const char *err)
 {
@@ -786,57 +845,8 @@ static void on_dnsdist(h2o_socket_t *listener, const char *err)
   }
 
   *du->self = nullptr; // so we don't clean up again in on_generator_dispose
-  if (du->status_code == 200) {
-    ++dsc->df->d_validresponses;
-    du->req->res.status = 200;
-
-    //    struct dnsheader* dh = (struct dnsheader*)du->query.c_str();
-    //    cout<<"Attempt to send out "<<du->query.size()<<" bytes over https, TC="<<dh->tc<<", RCODE="<<dh->rcode<<", qtype="<<du->qtype<<", req="<<(void*)du->req<<endl;
-
-    if (du->contentType.empty()) {
-      h2o_add_header(&du->req->pool, &du->req->res.headers, H2O_TOKEN_CONTENT_TYPE, nullptr, H2O_STRLIT("application/dns-message"));
-    }
-    else {
-      /* 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 ct = h2o_strdup(&du->req->pool, du->contentType.c_str(), du->contentType.size());
-      h2o_add_header(&du->req->pool, &du->req->res.headers, H2O_TOKEN_CONTENT_TYPE, nullptr, ct.base, ct.len);
-    }
 
-    du->req->res.content_length = du->response.size();
-    h2o_send_inline(du->req, du->response.c_str(), du->response.size());
-  }
-  else if (du->status_code >= 300 && du->status_code < 400) {
-    /* in that case the response is actually a URL */
-    /* we need to duplicate the URL because h2o uses it for the location header, keeping a pointer, and we will be deleted before the response has been sent */
-    h2o_iovec_t url = h2o_strdup(&du->req->pool, du->response.c_str(), du->response.size());
-    h2o_send_redirect(du->req, du->status_code, getReasonFromStatusCode(du->status_code).c_str(), url.base, url.len);
-    ++dsc->df->d_redirectresponses;
-  }
-  else {
-    if (!du->response.empty()) {
-      h2o_send_error_generic(du->req, du->status_code, getReasonFromStatusCode(du->status_code).c_str(), du->response.c_str(), 0);
-    }
-    else {
-      switch(du->status_code) {
-      case 400:
-        h2o_send_error_400(du->req, getReasonFromStatusCode(du->status_code).c_str(), "invalid DNS query" , 0);
-        break;
-      case 403:
-        h2o_send_error_403(du->req, getReasonFromStatusCode(du->status_code).c_str(), "dns query not allowed", 0);
-        break;
-      case 502:
-        h2o_send_error_502(du->req, getReasonFromStatusCode(du->status_code).c_str(), "no downstream server available", 0);
-        break;
-      case 500:
-        /* fall-through */
-      default:
-        h2o_send_error_500(du->req, getReasonFromStatusCode(du->status_code).c_str(), "Internal Server Error", 0);
-        break;
-      }
-    }
-
-    ++dsc->df->d_errorresponses;
-  }
+  handleResponse(*dsc->df, du->req, du->status_code, du->response, du->contentType);
 
   delete du;
 }
index ddcb2437806e56c0832ba5e6a99bbf32c95d8ecb..a058404be2f25d3ec66f2b17386c4fda8c5ae638 100644 (file)
@@ -4,11 +4,40 @@
 
 struct DOHServerConfig;
 
+class DOHResponseMapEntry
+{
+public:
+  DOHResponseMapEntry(const std::string& regex, uint16_t status, const std::string& content): d_regex(regex), d_content(content), d_status(status)
+  {
+  }
+
+  bool matches(const std::string& path) const
+  {
+    return d_regex.match(path);
+  }
+
+  uint16_t getStatusCode() const
+  {
+    return d_status;
+  }
+
+  const std::string& getContent() const
+  {
+    return d_content;
+  }
+
+private:
+  Regex d_regex;
+  std::string d_content;
+  uint16_t d_status;
+};
+
 struct DOHFrontend
 {
   std::shared_ptr<DOHServerConfig> d_dsc{nullptr};
   std::vector<std::pair<std::string, std::string>> d_certKeyPairs;
   std::vector<std::string> d_ocspFiles;
+  std::vector<std::shared_ptr<DOHResponseMapEntry>> d_responsesMap;
   std::string d_ciphers;
   std::string d_ciphers13;
   std::string d_serverTokens{"h2o/dnsdist"};
@@ -30,7 +59,7 @@ struct DOHFrontend
   std::atomic<uint64_t> d_postqueries;    // valid DNS queries received via POST
   std::atomic<uint64_t> d_badrequests;     // request could not be converted to dns query
   std::atomic<uint64_t> d_errorresponses; // dnsdist set 'error' on response
-    std::atomic<uint64_t> d_redirectresponses; // dnsdist set 'redirect' on response
+  std::atomic<uint64_t> d_redirectresponses; // dnsdist set 'redirect' on response
   std::atomic<uint64_t> d_validresponses; // valid responses sent out
 
   struct HTTPVersionStats
index bf4b990c3eb8673e5eaac372078e9362aa4148c4..7b7e652cead5b721b287576f9c81eac6fc373124 100644 (file)
@@ -148,6 +148,8 @@ class TestDOH(DNSDistDOHTest):
     newServer{address="127.0.0.1:%s"}
 
     addDOHLocal("127.0.0.1:%s", "%s", "%s", { "/" }, {customResponseHeaders={["access-control-allow-origin"]="*",["user-agent"]="derp"}})
+    dohFE = getDOHFrontend(0)
+    dohFE:setResponsesMap({newDOHResponseMapEntry('^/coffee$', 418, 'C0FFEE')})
 
     addAction("drop.doh.tests.powerdns.com.", DropAction())
     addAction("refused.doh.tests.powerdns.com.", RCodeAction(DNSRCode.REFUSED))
@@ -562,6 +564,38 @@ class TestDOH(DNSDistDOHTest):
         self.assertEquals(self._rcode, 200)
         self.assertTrue('content-type: text/plain' in self._response_headers.decode())
 
+    def testHTTPEarlyResponse(self):
+        """
+        DOH: HTTP Early Response
+        """
+        url = self._dohBaseURL + 'coffee'
+        conn = self.openDOHConnection(self._dohServerPort, caFile=self._caCert, timeout=2.0)
+        conn.setopt(pycurl.URL, url)
+        conn.setopt(pycurl.RESOLVE, ["%s:%d:127.0.0.1" % (self._serverName, self._dohServerPort)])
+        conn.setopt(pycurl.SSL_VERIFYPEER, 1)
+        conn.setopt(pycurl.SSL_VERIFYHOST, 2)
+        conn.setopt(pycurl.CAINFO, self._caCert)
+        data = conn.perform_rb()
+        rcode = conn.getinfo(pycurl.RESPONSE_CODE)
+
+        self.assertEquals(rcode, 418)
+        self.assertEquals(data, b'C0FFEE')
+
+        conn = self.openDOHConnection(self._dohServerPort, caFile=self._caCert, timeout=2.0)
+        conn.setopt(pycurl.URL, url)
+        conn.setopt(pycurl.RESOLVE, ["%s:%d:127.0.0.1" % (self._serverName, self._dohServerPort)])
+        conn.setopt(pycurl.SSL_VERIFYPEER, 1)
+        conn.setopt(pycurl.SSL_VERIFYHOST, 2)
+        conn.setopt(pycurl.CAINFO, self._caCert)
+        conn.setopt(pycurl.POST, True)
+        data = ''
+        conn.setopt(pycurl.POSTFIELDS, data)
+
+        data = conn.perform_rb()
+        rcode = conn.getinfo(pycurl.RESPONSE_CODE)
+        self.assertEquals(rcode, 418)
+        self.assertEquals(data, b'C0FFEE')
+
 class TestDOHAddingECS(DNSDistDOHTest):
 
     _serverKey = 'server.key'