]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
dnsdist: Add DNS over HTTPS support based on libh2o
authorRemi Gacogne <remi.gacogne@powerdns.com>
Mon, 15 Apr 2019 14:13:00 +0000 (16:13 +0200)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Mon, 15 Apr 2019 14:13:00 +0000 (16:13 +0200)
16 files changed:
builder-support/debian/dnsdist/debian-stretch/rules
builder-support/dockerfiles/Dockerfile.debbuild-prepare
builder-support/dockerfiles/Dockerfile.rpmbuild
builder-support/specs/dnsdist.spec
pdns/dnsdist-carbon.cc
pdns/dnsdist-lua-rules.cc
pdns/dnsdist-lua.cc
pdns/dnsdist.cc
pdns/dnsdist.hh
pdns/dnsdistdist/Makefile.am
pdns/dnsdistdist/configure.ac
pdns/dnsdistdist/dnsdist-rules.hh
pdns/dnsdistdist/doh.cc [new file with mode: 0644]
pdns/dnsdistdist/doh.hh [new file with mode: 0644]
pdns/dnsdistdist/m4/dnsdist_enable_doh.m4 [new file with mode: 0644]
pdns/dnsdistdist/m4/pdns_check_libh2o_evloop.m4 [new file with mode: 0644]

index f5a19a283f55ad4e25007f0b8ba77e42441c1d0a..9d870ed196703f53ca92f9771177934af900688d 100755 (executable)
@@ -28,6 +28,7 @@ override_dh_auto_clean:
        dh_auto_clean
 
 override_dh_auto_configure:
+       PKG_CONFIG_PATH=/opt/lib/pkgconfig \
        ./configure \
          --host=$(DEB_HOST_GNU_TYPE) \
          --build=$(DEB_BUILD_GNU_TYPE) \
@@ -37,6 +38,7 @@ override_dh_auto_configure:
          --infodir=\$${prefix}/share/info \
          --libdir='$${prefix}/lib/$(DEB_HOST_MULTIARCH)' \
          --libexecdir='$${prefix}/lib' \
+         --enable-dns-over-https \
          --enable-dns-over-tls \
          --enable-dnscrypt \
          --enable-dnstap \
index f466341dfc884033e793217ba945efb74f89cf98..22a27b06d99632b16f9b7db7e86a0f648805075c 100644 (file)
@@ -24,5 +24,14 @@ RUN tar xvf /sdist/pdns-recursor-${BUILDER_VERSION}.tar.bz2
 @ENDIF
 
 @IF [ ! -z "$M_dnsdist" ]
+RUN if grep 'VERSION="9 (stretch)"' /etc/os-release; then \
+  mkdir /libh2o && cd /libh2o && \
+  apt-get install -q -y curl libssl-dev zlib1g-dev cmake && \
+  curl -L https://github.com/h2o/h2o/archive/v2.2.5.tar.gz | tar xz && \
+  CFLAGS='-fPIC' cmake -DWITH_PICOTLS=off -DWITH_BUNDLED_SSL=off -DWITH_MRUBY=off -DCMAKE_INSTALL_PREFIX=/opt ./h2o-2.2.5 && \
+  make install && \
+  cd /pdns; \
+  fi
+
 RUN tar xvf /sdist/dnsdist-${BUILDER_VERSION}.tar.bz2
 @ENDIF
index 91a2bd57b721ab4233e7ba89af8d6c33fc4e72eb..3d8ac4a7ef333756bd571a59cec78776d50910f9 100644 (file)
@@ -41,6 +41,12 @@ RUN if $(grep -q 'release 6' /etc/redhat-release); then \
 RUN if $(grep -q 'release 6' /etc/redhat-release); then \
       scl enable devtoolset-7 -- builder/helpers/build-specs.sh builder-support/specs/dnsdist.spec; \
     else \
+      mkdir /libh2o && cd /libh2o && \
+      yum install -y curl openssl-devel cmake && \
+      curl -L https://github.com/h2o/h2o/archive/v2.2.5.tar.gz | tar xz && \
+      CFLAGS='-fPIC' cmake -DWITH_PICOTLS=off -DWITH_BUNDLED_SSL=off -DWITH_MRUBY=off -DCMAKE_INSTALL_PREFIX=/opt ./h2o-2.2.5 && \
+      make install && \
+      cd /pdns && \
       builder/helpers/build-specs.sh builder-support/specs/dnsdist.spec; \
     fi
 @ENDIF
index 1aa93848a4ab42fa26bd4f30b7fbf650d61f8f5f..cb476c49bb2aac4ddf64035c9ccf87505a34e795 100644 (file)
@@ -103,9 +103,11 @@ sed -i '/^ExecStart/ s/dnsdist/dnsdist -u dnsdist -g dnsdist/' dnsdist.service.i
   --with-libcap \
   --with-libsodium \
   --enable-dnscrypt \
+  --enable-dns-over-https \
   --enable-systemd --with-systemd=/lib/systemd/system \
   --with-re2 \
-  --with-net-snmp
+  --with-net-snmp \
+  PKG_CONFIG_PATH=/opt/lib64/pkgconfig
 %endif
 
 %if 0%{?el6}
index 1fa49f58468010255209bff4ab789ac568a073d8..14b72e048e676645c7aa47c339bbd16ea494f2cf 100644 (file)
@@ -146,6 +146,39 @@ try
           }
         }
 
+#ifdef HAVE_DNS_OVER_HTTPS
+        {
+          const string base = "dnsdist." + hostname + ".main.doh.";
+          for(const auto& doh : g_dohlocals) {
+            string name = doh->d_local.toStringWithPort();
+            boost::replace_all(name, ".", "_");
+            boost::replace_all(name, ":", "_");
+            boost::replace_all(name, "[", "_");
+            boost::replace_all(name, "]", "_");
+
+            vector<pair<const char*, const std::atomic<uint64_t>&>> v{
+              {"http-connects", doh->d_httpconnects},
+              {"http1-queries", doh->d_http1queries},
+              {"http2-queries", doh->d_http2queries},
+              {"tls10-queries", doh->d_tls10queries},
+              {"tls11-queries", doh->d_tls11queries},
+              {"tls12-queries", doh->d_tls12queries},
+              {"tls13-queries", doh->d_tls13queries},
+              {"tls-unknown-queries", doh->d_tlsUnknownqueries},
+              {"get-queries", doh->d_getqueries},
+              {"post-queries", doh->d_postqueries},
+              {"bad-requests", doh->d_badrequests},
+              {"error-responses", doh->d_errorresponses},
+              {"valid-responses", doh->d_validresponses}
+            };
+
+            for(const auto& item : v) {
+              str<<base<<name<<"."<<item.first << " " << item.second << " " << now <<"\r\n";
+            }
+          }
+        }
+#endif /* HAVE_DNS_OVER_HTTPS */
+
         {
           WriteLock wl(&g_qcount.queryLock);
           std::string qname;
index 4b4114bc7ee8ac8ffd22ea4b647605460e5f8cca..5fbf7643dbb19ef4e2fe312b94cee25d7538674b 100644 (file)
@@ -280,6 +280,15 @@ void setupLuaRules()
       return std::shared_ptr<DNSRule>(new RegexRule(str));
     });
 
+#ifdef HAVE_DNS_OVER_HTTPS
+  g_lua.writeFunction("HTTPHeaderRule", [](const std::string& header, const std::string& regex) {
+      return std::shared_ptr<DNSRule>(new HTTPHeaderRule(header, regex));
+    });
+  g_lua.writeFunction("HTTPPathRule", [](const std::string& path) {
+      return std::shared_ptr<DNSRule>(new HTTPPathRule(path));
+    });
+#endif
+
 #ifdef HAVE_RE2
   g_lua.writeFunction("RE2Rule", [](const std::string& str) {
       return std::shared_ptr<DNSRule>(new RE2Rule(str));
index ad4c55472354e2a6f47df8652d8bf0027a534fc3..bb68d91cb5b0c9758fb2d7630de0baba8c7abca9 100644 (file)
@@ -1650,6 +1650,43 @@ void setupLuaConfig(bool client)
     setSyslogFacility(facility);
   });
 
+  g_lua.writeFunction("addDOHLocal", [client](const std::string& addr, const std::string& certFile, const std::string& keyFile, boost::optional<vector<pair<int, std::string> > > urls, boost::optional<localbind_t> vars) {
+    if (client) {
+      return;
+    }
+#ifdef HAVE_DNS_OVER_HTTPS
+    setLuaSideEffect();
+    if (g_configurationDone) {
+      g_outputBuffer="addDOHLocal cannot be used at runtime!\n";
+      return;
+    }
+    auto frontend = std::make_shared<DOHFrontend>();
+    frontend->d_certFile = certFile;
+    frontend->d_keyFile = keyFile;
+    frontend->d_local = ComboAddress(addr, 443);
+    if(urls && !urls->empty()) {
+      for(const auto& p : *urls) {
+        frontend->d_urls.push_back(p.second);
+      }
+    }
+    else {
+      frontend->d_urls = {"/"};
+    }
+
+    if(vars) {
+      if (vars->count("idleTimeout")) {
+        frontend->d_idleTimeout = boost::get<int>((*vars)["idleTimeout"]);
+      }
+    }
+    g_dohlocals.push_back(frontend);
+    auto cs = std::unique_ptr<ClientState>(new ClientState(frontend->d_local, true, false, 0, "", {}));
+    cs->dohFrontend = frontend;
+    g_frontends.push_back(std::move(cs));
+#else
+    g_outputBuffer="DNS over HTTPS support is not present!\n";
+#endif
+  });
+
   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) {
         if (client)
           return;
index f7bfc1dee545f193182f8a758dbdfe520783a99c..28a9d5fc5505645b5806137a4a54be294ec7f2e5 100644 (file)
@@ -93,6 +93,7 @@ GlobalStateHolder<NetmaskGroup> g_ACL;
 string g_outputBuffer;
 
 std::vector<std::shared_ptr<TLSFrontend>> g_tlslocals;
+std::vector<std::shared_ptr<DOHFrontend>> g_dohlocals;
 std::vector<std::shared_ptr<DNSCryptContext>> g_dnsCryptLocals;
 #ifdef HAVE_EBPF
 shared_ptr<BPFFilter> g_defaultBPFFilter;
@@ -486,7 +487,7 @@ static bool sendUDPResponse(int origFD, const char* response, const uint16_t res
 }
 
 
-static int pickBackendSocketForSending(std::shared_ptr<DownstreamState>& state)
+int pickBackendSocketForSending(std::shared_ptr<DownstreamState>& state)
 {
   return state->sockets[state->socketsOffset++ % state->sockets.size()];
 }
@@ -537,13 +538,14 @@ try {
         uint16_t responseLen = static_cast<uint16_t>(got);
         queryId = dh->id;
 
-        if(queryId >= dss->idStates.size())
+        if(queryId >= dss->idStates.size()) {
           continue;
+        }
 
         IDState* ids = &dss->idStates[queryId];
         int origFD = ids->origFD;
 
-        if(origFD < 0) // duplicate
+        if(origFD < 0 && ids->du == nullptr) // duplicate
           continue;
 
         /* setting age to 0 to prevent the maintainer thread from
@@ -585,17 +587,30 @@ try {
         }
 
         if (ids->cs && !ids->cs->muted) {
-          ComboAddress empty;
-          empty.sin4.sin_family = 0;
-          /* if ids->destHarvested is false, origDest holds the listening address.
-             We don't want to use that as a source since it could be 0.0.0.0 for example. */
-          sendUDPResponse(origFD, response, responseLen, dr.delayMsec, ids->destHarvested ? ids->origDest : empty, ids->origRemote);
+          if (ids->du) {
+#ifdef HAVE_DNS_OVER_HTTPS
+            // DoH query
+            ids->du->query = std::string(response, responseLen);
+            if (send(ids->du->rsock, &ids->du, sizeof(ids->du), 0) != sizeof(ids->du)) {
+              delete ids->du;
+            }
+#endif /* HAVE_DNS_OVER_HTTPS */
+            ids->du = nullptr;
+          }
+          else {
+            ComboAddress empty;
+            empty.sin4.sin_family = 0;
+            /* if ids->destHarvested is false, origDest holds the listening address.
+               We don't want to use that as a source since it could be 0.0.0.0 for example. */
+            sendUDPResponse(origFD, response, responseLen, dr.delayMsec, ids->destHarvested ? ids->origDest : empty, ids->origRemote);
+          }
         }
 
         ++g_stats.responses;
 
         double udiff = ids->sentTime.udiff();
-        vinfolog("Got answer from %s, relayed to %s, took %f usec", dss->remote.toStringWithPort(), dr.remote->toStringWithPort(), udiff);
+        vinfolog("Got answer from %s, relayed to %s%s, took %f usec", dss->remote.toStringWithPort(), ids->origRemote.toStringWithPort(),
+                 ids->du ? " (https)": "", udiff);
 
         struct timespec ts;
         gettime(&ts);
@@ -1198,7 +1213,7 @@ static bool applyRulesToQuery(LocalHolders& holders, DNSQuestion& dq, string& po
   return true;
 }
 
-static ssize_t udpClientSendRequestToBackend(const std::shared_ptr<DownstreamState>& ss, const int sd, const char* request, const size_t requestLen, bool healthCheck=false)
+ssize_t udpClientSendRequestToBackend(const std::shared_ptr<DownstreamState>& ss, const int sd, const char* request, const size_t requestLen, bool healthCheck)
 {
   ssize_t result;
 
@@ -1548,14 +1563,15 @@ static void processUDPQuery(ClientState& cs, LocalHolders& holders, const struct
     unsigned int idOffset = (ss->idOffset++) % ss->idStates.size();
     IDState* ids = &ss->idStates[idOffset];
     ids->age = 0;
+    ids->du = nullptr;
 
     int oldFD = ids->origFD.exchange(cs.udpFD);
     if(oldFD < 0) {
       // if we are reusing, no change in outstanding
-      ss->outstanding++;
+      ++ss->outstanding;
     }
     else {
-      ss->reuseds++;
+      ++ss->reuseds;
       ++g_stats.downstreamTimeouts;
     }
 
@@ -1584,7 +1600,7 @@ static void processUDPQuery(ClientState& cs, LocalHolders& holders, const struct
     ssize_t ret = udpClientSendRequestToBackend(ss, fd, query, dq.len);
 
     if(ret < 0) {
-      ss->sendErrors++;
+      ++ss->sendErrors;
       ++g_stats.downstreamSendErrors;
     }
 
@@ -2052,6 +2068,7 @@ static void healthChecksThread()
                don't go anywhere near it */
             continue;
           }
+          ids.du = nullptr;
           ids.age = 0;
           dss->reuseds++;
           --dss->outstanding;
@@ -2171,7 +2188,7 @@ static void checkFileDescriptorsLimits(size_t udpBindsCount, size_t tcpBindsCoun
 static void setUpLocalBind(std::unique_ptr<ClientState>& cs)
 {
   /* skip some warnings if there is an identical UDP context */
-  bool warn = cs->tcp == false || cs->tlsFrontend != nullptr;
+  bool warn = cs->tcp == false || cs->tlsFrontend != nullptr || cs->dohFrontend != nullptr;
   int& fd = cs->tcp == false ? cs->udpFD : cs->tcpFD;
   (void) warn;
 
@@ -2246,13 +2263,20 @@ static void setUpLocalBind(std::unique_ptr<ClientState>& cs)
     }
   }
 
+  if (cs->dohFrontend != nullptr) {
+    cs->dohFrontend->setup();
+  }
+
   SBind(fd, cs->local);
 
   if (cs->tcp) {
-    SListen(cs->tcpFD, 64);
+    SListen(cs->tcpFD, SOMAXCONN);
     if (cs->tlsFrontend != nullptr) {
       warnlog("Listening on %s for TLS", cs->local.toStringWithPort());
     }
+    else if (cs->dohFrontend != nullptr) {
+      warnlog("Listening on %s for DoH", cs->local.toStringWithPort());
+    }
     else if (cs->dnscryptCtx != nullptr) {
       warnlog("Listening on %s for DNSCrypt", cs->local.toStringWithPort());
     }
@@ -2438,6 +2462,9 @@ try
 #endif
       cout<<") ";
 #endif
+#ifdef HAVE_DNS_OVER_HTTPS
+      cout<<"dns-over-https(DOH) ";
+#endif
 #ifdef HAVE_DNSCRYPT
       cout<<"dnscrypt ";
 #endif
@@ -2547,8 +2574,8 @@ try
 
   if (!g_cmdLine.locals.empty()) {
     for (auto it = g_frontends.begin(); it != g_frontends.end(); ) {
-      /* TLS and DNSCrypt frontends are separate */
-      if ((*it)->tlsFrontend == nullptr && (*it)->dnscryptCtx == nullptr) {
+      /* DoH, DoT and DNSCrypt frontends are separate */
+      if ((*it)->dohFrontend == nullptr && (*it)->tlsFrontend == nullptr && (*it)->dnscryptCtx == nullptr) {
         it = g_frontends.erase(it);
       }
       else {
@@ -2679,6 +2706,13 @@ try
   }
 
   for(auto& cs : g_frontends) {
+    if (cs->dohFrontend != nullptr) {
+#ifdef HAVE_DNS_OVER_HTTPS
+      std::thread t1(dohThread, cs.get());
+      t1.detach();
+#endif /* HAVE_DNS_OVER_HTTPS */
+      continue;
+    }
     if (cs->udpFD >= 0) {
       thread t1(udpClientThread, cs.get());
       if (!cs->cpus.empty()) {
index 71651495fa54f218a4a2ca2c2e51927fdbf6950a..3d32ee2a10466a689beb77180f1a28b34bc8a142 100644 (file)
@@ -40,6 +40,7 @@
 #include "dnsdist-cache.hh"
 #include "dnsdist-dynbpf.hh"
 #include "dnsname.hh"
+#include "doh.hh"
 #include "ednsoptions.hh"
 #include "gettime.hh"
 #include "iputils.hh"
@@ -83,6 +84,7 @@ struct DNSQuestion
   std::shared_ptr<DNSDistPacketCache> packetCache{nullptr};
   struct dnsheader* dh{nullptr};
   const struct timespec* queryTime{nullptr};
+  struct DOHUnit* du{nullptr};
   size_t size;
   unsigned int consumed{0};
   int delayMsec{0};
@@ -553,6 +555,7 @@ struct IDState
   std::shared_ptr<DNSDistPacketCache> packetCache{nullptr};
   std::shared_ptr<QTag> qTag{nullptr};
   const ClientState* cs{nullptr};
+  DOHUnit* du{nullptr};
   uint32_t cacheKey;                                          // 4
   uint32_t cacheKeyNoECS;                                     // 4
   uint16_t age;                                               // 4
@@ -595,6 +598,7 @@ struct ClientState
   ComboAddress local;
   std::shared_ptr<DNSCryptContext> dnscryptCtx{nullptr};
   std::shared_ptr<TLSFrontend> tlsFrontend{nullptr};
+  std::shared_ptr<DOHFrontend> dohFrontend{nullptr};
   std::string interface;
   std::atomic<uint64_t> queries{0};
   std::atomic<uint64_t> tcpDiedReadingQuery{0};
@@ -623,7 +627,10 @@ struct ClientState
   {
     std::string result = udpFD != -1 ? "UDP" : "TCP";
 
-    if (tlsFrontend) {
+    if (dohFrontend) {
+      result += " (DNS over HTTPS)";
+    }
+    else if (tlsFrontend) {
       result += " (DNS over TLS)";
     }
     else if (dnscryptCtx) {
@@ -1023,6 +1030,7 @@ extern ComboAddress g_serverControl; // not changed during runtime
 
 extern std::vector<std::tuple<ComboAddress, bool, bool, int, std::string, std::set<int>>> g_locals; // not changed at runtime (we hope XXX)
 extern std::vector<shared_ptr<TLSFrontend>> g_tlslocals;
+extern std::vector<shared_ptr<DOHFrontend>> g_dohlocals;
 extern std::vector<std::unique_ptr<ClientState>> g_frontends;
 extern bool g_truncateTC;
 extern bool g_fixupCase;
@@ -1103,6 +1111,9 @@ void setWebserverCustomHeaders(const boost::optional<std::map<std::string, std::
 
 void dnsdistWebserverThread(int sock, const ComboAddress& local);
 void tcpAcceptorThread(void* p);
+#ifdef HAVE_DNS_OVER_HTTPS
+void dohThread(ClientState* cs);
+#endif /* HAVE_DNS_OVER_HTTPS */
 
 void setLuaNoSideEffect(); // if nothing has been declared, set that there are no side effects
 void setLuaSideEffect();   // set to report a side effect, cancelling all _no_ side effect calls
@@ -1136,3 +1147,6 @@ ProcessQueryResult processQuery(DNSQuestion& dq, ClientState& cs, LocalHolders&
 
 DNSResponse makeDNSResponseFromIDState(IDState& ids, struct dnsheader* dh, size_t bufferSize, uint16_t responseLen, bool isTCP);
 void setIDStateFromDNSQuestion(IDState& ids, DNSQuestion& dq, DNSName&& qname);
+
+int pickBackendSocketForSending(std::shared_ptr<DownstreamState>& state);
+ssize_t udpClientSendRequestToBackend(const std::shared_ptr<DownstreamState>& ss, const int sd, const char* request, const size_t requestLen, bool healthCheck=false);
index 3c438681454a1f657f410cf7d41b5ae88045851d..f15cf5af446a1364b9a312306ccb6269f2ae20fe 100644 (file)
@@ -49,6 +49,16 @@ if HAVE_LIBCRYPTO
 AM_CPPFLAGS += $(LIBCRYPTO_INCLUDES)
 endif
 
+if HAVE_DNS_OVER_HTTPS
+if HAVE_LIBSSL
+AM_CPPFLAGS += $(LIBSSL_CFLAGS)
+endif
+
+if HAVE_LIBH2OEVLOOP
+AM_CPPFLAGS += $(LIBH2OEVLOOP_CFLAGS)
+endif
+endif
+
 EXTRA_DIST=COPYING \
           dnslabeltext.rl \
           dnsdistconf.lua \
@@ -124,6 +134,7 @@ dnsdist_SOURCES = \
        dnsname.cc dnsname.hh \
        dnsparser.hh dnsparser.cc \
        dnswriter.cc dnswriter.hh \
+       doh.hh \
        dolog.hh \
        ednsoptions.cc ednsoptions.hh \
        ednscookies.cc ednscookies.hh \
@@ -194,6 +205,19 @@ dnsdist_LDADD += $(LIBSSL_LIBS)
 endif
 endif
 
+if HAVE_DNS_OVER_HTTPS
+dnsdist_SOURCES += doh.cc
+
+if HAVE_LIBH2OEVLOOP
+dnsdist_LDADD += $(LIBH2OEVLOOP_LIBS)
+endif
+
+if HAVE_LIBSSL
+dnsdist_LDADD += $(LIBSSL_LIBS)
+endif
+
+endif
+
 if !HAVE_LUA_HPP
 BUILT_SOURCES += lua.hpp
 nodist_dnsdist_SOURCES = lua.hpp
index 7d8014c955abcc74a910a64bf9a4a7aceb031c48..456879f3353c844b91c0253028900901746a5f59 100644 (file)
@@ -65,17 +65,30 @@ AM_CONDITIONAL([HAVE_GNUTLS], [false])
 AM_CONDITIONAL([HAVE_LIBSSL], [false])
 
 PDNS_CHECK_LIBCRYPTO
+DNSDIST_WITH_LIBSSL
 
 DNSDIST_ENABLE_DNS_OVER_TLS
 
 AS_IF([test "x$enable_dns_over_tls" != "xno"], [
   DNSDIST_WITH_GNUTLS
-  DNSDIST_WITH_LIBSSL
+
   AS_IF([test "$HAVE_GNUTLS" = "0" -a "$HAVE_LIBSSL" = "0"], [
     AC_MSG_ERROR([DNS over TLS support requested but neither GnuTLS nor OpenSSL are available])
   ])
 ])
 
+DNSDIST_ENABLE_DNS_OVER_HTTPS
+PDNS_CHECK_LIBH2OEVLOOP
+AS_IF([test "x$enable_dns_over_https" != "xno"], [
+  AS_IF([test "$HAVE_LIBH2OEVLOOP" = "0"], [
+    AC_MSG_ERROR([DNS over HTTPS support requested but libh2o-evloop was not found])
+  ])
+
+  AS_IF([test "$HAVE_LIBSSL" = "0"], [
+    AC_MSG_ERROR([DNS over HTTPS support requested but OpenSSL was not found])
+  ])
+])
+
 AX_CXX_COMPILE_STDCXX_11([ext], [mandatory])
 
 AC_MSG_CHECKING([whether we will enable compiler security checks])
@@ -177,15 +190,25 @@ AS_IF([test "x$NET_SNMP_LIBS" != "x"],
   [AC_MSG_NOTICE([SNMP: yes])],
   [AC_MSG_NOTICE([SNMP: no])]
 )
+AS_IF([test "x$enable_dns_over_tls" != "xno"],
+  [AC_MSG_NOTICE([DNS over TLS: yes])],
+  [AC_MSG_NOTICE([DNS over TLS: no])]
+)
+AS_IF([test "x$enable_dns_over_https" != "xno"],
+  [AC_MSG_NOTICE([DNS over HTTPS (DoH): yes])],
+  [AC_MSG_NOTICE([DNS over HTTPS (DoH): no])]
+)
 AS_IF([test "x$enable_dns_over_tls" != "xno"], [
-  AC_MSG_NOTICE([DNS over TLS: yes])
   AS_IF([test "x$GNUTLS_LIBS" != "x"],
     [AC_MSG_NOTICE([GnuTLS: yes])],
-    [AC_MSG_NOTICE([GnuTLS: no])])
+    [AC_MSG_NOTICE([GnuTLS: no])]
+  )]
+)
+AS_IF([test "x$enable_dns_over_tls" != "xno" -o "x$enable_dns_over_https" != "xno"], [
   AS_IF([test "x$LIBSSL_LIBS" != "x"],
     [AC_MSG_NOTICE([OpenSSL: yes])],
-    [AC_MSG_NOTICE([OpenSSL: no])])
-  ],
-  [AC_MSG_NOTICE([DNS over TLS: no])]
+    [AC_MSG_NOTICE([OpenSSL: no])]
+  )]
 )
+
 AC_MSG_NOTICE([])
index bbc4ff2056735d74af71f92d0c8396ad405f67a3..a25d8572c058d9b63f2356e0254a5f919ab05ada 100644 (file)
@@ -501,6 +501,29 @@ private:
 };
 #endif
 
+#ifdef HAVE_DNS_OVER_HTTPS
+class HTTPHeaderRule : public DNSRule
+{
+public:
+  HTTPHeaderRule(const std::string& header, const std::string& regex);
+  bool matches(const DNSQuestion* dq) const override;
+  string toString() const override;
+private:
+  string d_header;
+  Regex d_regex;
+  string d_visual;
+};
+
+class HTTPPathRule : public DNSRule
+{
+public:
+  HTTPPathRule(const std::string& path);
+  bool matches(const DNSQuestion* dq) const override;
+  string toString() const override;
+private:
+  string d_path;
+};
+#endif
 
 class SuffixMatchNodeRule : public DNSRule
 {
diff --git a/pdns/dnsdistdist/doh.cc b/pdns/dnsdistdist/doh.cc
new file mode 100644 (file)
index 0000000..c3a7409
--- /dev/null
@@ -0,0 +1,603 @@
+#define H2O_USE_EPOLL 1
+#include <errno.h>
+#include <iostream>
+#include "h2o.h"
+#include "h2o/http1.h"
+#include "h2o/http2.h"
+#include "base64.hh"
+#include "dnsname.hh"
+#undef CERT
+#include "dnsdist.hh"
+#include "misc.hh"
+#include <thread>
+#include "dns.hh"
+#include "dolog.hh"
+#include "dnsdist-ecs.hh"
+#include "dnsdist-rules.hh"
+#include "dnsdist-xpf.hh"
+#include <boost/algorithm/string.hpp>
+
+using namespace std;
+
+/* So, how does this work. We use h2o for our http2 and TLS needs.
+   If the operator has configured multiple IP addresses to listen on,
+   we launch multiple h2o listener threads. We can hook in to multiple
+   URLs though on the same IP. There is no SNI yet (I think).
+
+   h2o is event driven, so we get callbacks if a new DNS query arrived.
+   When it does, we do some minimal parsing on it, and send it on to the
+   dnsdist worker thread which we also launched.
+
+   This dnsdist worker thread injects the query into the normal dnsdist flow
+   (as a datagram over a socketpair). The response also goes back over a
+   (different) socketpair, where we pick it up and deliver it back to h2o.
+
+   For coordination, we use the h2o socket multiplexer, which is sensitive to our
+   socketpair too.
+*/
+
+/* h2o notes.
+   Paths and parameters etc just *happen* to be null-terminated in HTTP2.
+   They are not in HTTP1. So you MUST use the length field!
+*/
+
+// we create one of these per thread, and pass around a pointer to it
+// through the bowels of h2o
+struct DOHServerConfig
+{
+  DOHServerConfig(ClientState* cs_): cs(cs_), df(cs_->dohFrontend)
+  {
+    memset(&h2o_accept_ctx, 0, sizeof(h2o_accept_ctx));
+
+    if(socketpair(AF_LOCAL, SOCK_DGRAM, 0, dohquerypair) < 0) {
+      unixDie("Creating a socket pair for DNS over HTTPS");
+    }
+
+    if (socketpair(AF_LOCAL, SOCK_DGRAM, 0, dohresponsepair) < 0) {
+      close(dohquerypair[0]);
+      close(dohquerypair[1]);
+      unixDie("Creating a socket pair for DNS over HTTPS");
+    }
+
+    h2o_config_init(&h2o_config);
+    h2o_config.http2.idle_timeout = df->d_idleTimeout * 1000;
+  }
+
+  h2o_globalconf_t h2o_config;
+  h2o_context_t h2o_ctx;
+  h2o_accept_ctx_t h2o_accept_ctx;
+  ClientState* cs{nullptr};
+  std::shared_ptr<DOHFrontend> df{nullptr};
+  int dohquerypair[2]{-1,-1};
+  int dohresponsepair[2]{-1,-1};
+};
+
+/* this duplicates way too much from the UDP handler. Sorry.
+   this function calls 'return -1' to drop a query without sending it
+   caller should make sure HTTPS thread hears of that
+*/
+
+static int processDOHQuery(DOHUnit* du)
+{
+  LocalHolders holders;
+  uint16_t queryId = 0;
+  try {
+    if(!du->req) {
+      // we got closed meanwhile. XXX small race condition here
+      return -1;
+    }
+    DOHServerConfig* dsc = (DOHServerConfig*)du->req->conn->ctx->storage.entries[0].data;
+    ClientState& cs = *dsc->cs;
+
+    if (du->query.size() < sizeof(dnsheader)) {
+      ++g_stats.nonCompliantQueries;
+      return -1;
+    }
+
+    if(!holders.acl->match(du->remote)) {
+      vinfolog("Query from %s (DoH) dropped because of ACL", du->remote.toStringWithPort());
+      ++g_stats.aclDrops;
+      return -1;
+    }
+
+    ++cs.queries;
+    ++g_stats.queries;
+
+    /* we need an accurate ("real") value for the response and
+       to store into the IDS, but not for insertion into the
+       rings for example */
+    struct timespec queryRealTime;
+    gettime(&queryRealTime, true);
+    uint16_t len = du->query.length();
+    /* allocate a bit more memory to be able to spoof the content,
+       or to add ECS without allocating a new buffer */
+    du->query.resize(du->query.size() + 512);
+    size_t bufferSize = du->query.size();
+    auto query = const_cast<char*>(du->query.c_str());
+    struct dnsheader* dh = reinterpret_cast<struct dnsheader*>(query);
+
+    if (!checkQueryHeaders(dh)) {
+      return -1; // drop
+    }
+
+    uint16_t qtype, qclass;
+    unsigned int consumed = 0;
+    DNSName qname(query, len, sizeof(dnsheader), false, &qtype, &qclass, &consumed);
+    DNSQuestion dq(&qname, qtype, qclass, consumed, &du->dest, &du->remote, dh, bufferSize, len, false, &queryRealTime);
+    dq.ednsAdded = du->ednsAdded;
+    dq.du = du;
+    queryId = ntohs(dh->id);
+
+    std::shared_ptr<DownstreamState> ss{nullptr};
+    auto result = processQuery(dq, cs, holders, ss);
+
+    if (result == ProcessQueryResult::Drop) {
+      return -1;
+    }
+
+    if (result == ProcessQueryResult::SendAnswer) {
+      du->query = std::string(reinterpret_cast<char*>(dq.dh), dq.len);
+      send(du->rsock, &du, sizeof(du), 0);
+      return 0;
+    }
+
+    if (result != ProcessQueryResult::PassToBackend || ss == nullptr) {
+      return -1;
+    }
+
+    unsigned int idOffset = (ss->idOffset++) % ss->idStates.size();
+    IDState* ids = &ss->idStates[idOffset];
+    ids->age = 0;
+    ids->du = du;
+
+    int oldFD = ids->origFD.exchange(cs.udpFD);
+    if(oldFD < 0) {
+      // if we are reusing, no change in outstanding
+      ++ss->outstanding;
+    }
+    else {
+      ++ss->reuseds;
+      ++g_stats.downstreamTimeouts;
+    }
+
+    ids->cs = &cs;
+    ids->origID = dh->id;
+    setIDStateFromDNSQuestion(*ids, dq, std::move(qname));
+
+    /* If we couldn't harvest the real dest addr, still
+       write down the listening addr since it will be useful
+       (especially if it's not an 'any' one).
+       We need to keep track of which one it is since we may
+       want to use the real but not the listening addr to reply.
+    */
+    if (du->dest.sin4.sin_family != 0) {
+      ids->origDest = du->dest;
+      ids->destHarvested = true;
+    }
+    else {
+      ids->origDest = cs.local;
+      ids->destHarvested = false;
+    }
+
+    dh->id = idOffset;
+
+    int fd = pickBackendSocketForSending(ss);
+    ssize_t ret = udpClientSendRequestToBackend(ss, fd, query, dq.len);
+
+    if(ret < 0) {
+      ++ss->sendErrors;
+      ++g_stats.downstreamSendErrors;
+    }
+
+    vinfolog("Got query for %s|%s from %s (https), relayed to %s", ids->qname.toString(), QType(ids->qtype).getName(), du->remote.toStringWithPort(), ss->getName());
+  }
+  catch(const std::exception& e) {
+    vinfolog("Got an error in DOH question thread while parsing a query from %s, id %d: %s", du->remote.toStringWithPort(), queryId, e.what());
+    return -1;
+  }
+  return 0;
+}
+
+static h2o_pathconf_t *register_handler(h2o_hostconf_t *hostconf, const char *path, int (*on_req)(h2o_handler_t *, h2o_req_t *))
+{
+  h2o_pathconf_t *pathconf = h2o_config_register_path(hostconf, path, 0);
+  h2o_handler_t *handler = h2o_create_handler(pathconf, sizeof(*handler));
+  handler->on_req = on_req;
+  return pathconf;
+}
+
+/* this is called by h2o when our request dies.
+   We use this to signal to the 'du' that this req is no longer alive */
+static void on_generator_dispose(void *_self)
+{
+  DOHUnit** du = (DOHUnit**)_self;
+  if(*du) { // if 0, on_dnsdist cleaned up du already
+//    cout << "du "<<(void*)*du<<" containing req "<<(*du)->req<<" got killed"<<endl;
+    (*du)->req = nullptr;
+  }
+}
+
+static void doh_dispatch_query(DOHServerConfig* dsc, h2o_handler_t* self, h2o_req_t* req, std::string&& query, ComboAddress& remote)
+{
+  try {
+    auto du = std::unique_ptr<DOHUnit>(new DOHUnit);
+    du->self = reinterpret_cast<DOHUnit**>(h2o_mem_alloc_shared(&req->pool, sizeof(*self), on_generator_dispose));
+    uint16_t qtype;
+    DNSName qname(query.c_str(), query.size(), sizeof(dnsheader), false, &qtype);
+    du->req = req;
+    du->query = std::move(query);
+    du->remote = remote;
+    du->rsock = dsc->dohresponsepair[0];
+    du->qtype = qtype;
+    auto ptr = du.release();
+    *(ptr->self) = ptr;
+    try  {
+      if(send(dsc->dohquerypair[0], &ptr, sizeof(ptr), 0) != sizeof(ptr)) {
+        delete ptr;     // XXX but now what - will h2o time this out for us?
+        ptr = nullptr;
+      }
+    }
+    catch(...) {
+      delete ptr;
+    }
+  }
+  catch(const std::exception& e) {
+    vinfolog("Had error parsing DoH DNS packet from %s: %s", remote.toStringWithPort(), e.what());
+    h2o_send_error_400(req, "Bad Request", "dnsdist " VERSION " could not parse DNS query", 0);
+  }
+}
+
+/*
+   For GET, the base64url-encoded payload is in the 'dns' parameter, which might be the first parameter, or not.
+   For POST, the payload is the payload.
+ */
+static int doh_handler(h2o_handler_t *self, h2o_req_t *req)
+try
+{
+  // g_logstream<<(void*)req<<" doh_handler"<<endl;
+  if(!req->conn->ctx->storage.size) {
+    return 0; // although we might was well crash on this
+  }
+  h2o_socket_t* sock = req->conn->callbacks->get_socket(req->conn);
+  ComboAddress remote;
+  h2o_socket_getpeername(sock, reinterpret_cast<struct sockaddr*>(&remote));
+  DOHServerConfig* dsc = (DOHServerConfig*)req->conn->ctx->storage.entries[0].data;
+
+  if(auto tlsversion = h2o_socket_get_ssl_protocol_version(sock)) {
+    if(!strcmp(tlsversion, "TLSv1.0"))
+      ++dsc->df->d_tls10queries;
+    else if(!strcmp(tlsversion, "TLSv1.1"))
+      ++dsc->df->d_tls11queries;
+    else if(!strcmp(tlsversion, "TLSv1.2"))
+      ++dsc->df->d_tls12queries;
+    else if(!strcmp(tlsversion, "TLSv1.3"))
+      ++dsc->df->d_tls13queries;
+    else
+      ++dsc->df->d_tlsUnknownqueries;
+  }
+
+  string path(req->path.base, req->path.len);
+
+  if (h2o_memis(req->method.base, req->method.len, H2O_STRLIT("POST"))) {
+    ++dsc->df->d_postqueries;
+    if(req->version >= 0x0200)
+      ++dsc->df->d_http2queries;
+    else
+      ++dsc->df->d_http1queries;
+
+    std::string query;
+    query.reserve(req->entity.len + 512);
+    query.assign(req->entity.base, req->entity.len);
+    doh_dispatch_query(dsc, self, req, std::move(query), remote);
+  }
+  else if(req->query_at != SIZE_MAX && (req->path.len - req->query_at > 5)) {
+    auto pos = path.find("?dns=");
+    if(pos == string::npos)
+      pos = path.find("&dns=");
+    if(pos != string::npos) {
+      // need to base64url decode this
+      string sdns(path.substr(pos+5));
+      boost::replace_all(sdns,"-", "+");
+      boost::replace_all(sdns,"_", "/");
+      sdns.append(sdns.size() % 4, '='); // re-add padding that may have been missing
+
+      string decoded;
+      /* rough estimate so we hopefully don't need a need allocation later */
+      decoded.reserve(((sdns.size() * 3) / 4) + 512);
+      if(B64Decode(sdns, decoded) < 0) {
+        h2o_send_error_400(req, "Bad Request", "dnsdist " VERSION " could not decode BASE64-URL", 0);
+        ++dsc->df->d_badrequests;
+        return 0;
+      }
+      else {
+        ++dsc->df->d_getqueries;
+        if(req->version >= 0x0200)
+          ++dsc->df->d_http2queries;
+        else
+          ++dsc->df->d_http1queries;
+
+        doh_dispatch_query(dsc, self, req, std::move(decoded), remote);
+      }
+    }
+    else
+    {
+      vinfolog("HTTP request without DNS parameter: %s", req->path.base);
+      h2o_send_error_400(req, "Bad Request", "dnsdist " VERSION " could not find DNS parameter", 0);
+      ++dsc->df->d_badrequests;
+      return 0;
+    }
+  }
+  else {
+    h2o_send_error_400(req, "Bad Request", "dnsdist " VERSION " could not parse your request", 0);
+    ++dsc->df->d_badrequests;
+  }
+  return 0;
+}
+catch(const exception& e)
+{
+  errlog("DOH Handler function failed with error %s", e.what());
+  return 0;
+}
+
+HTTPHeaderRule::HTTPHeaderRule(const std::string& header, const std::string& regex)
+  :  d_regex(regex)
+{
+  d_header = toLower(header);
+  d_visual = "http[" + header+ "] ~ " + regex;
+
+}
+bool HTTPHeaderRule::matches(const DNSQuestion* dq) const
+{
+  if(!dq->du) {
+    return false;
+  }
+
+  for (unsigned int i = 0; i != dq->du->req->headers.size; ++i) {
+    if(std::string(dq->du->req->headers.entries[i].name->base, dq->du->req->headers.entries[i].name->len) == d_header &&
+       d_regex.match(std::string(dq->du->req->headers.entries[i].value.base, dq->du->req->headers.entries[i].value.len))) {
+      return true;
+    }
+  }
+  return false;
+}
+
+string HTTPHeaderRule::toString() const
+{
+  return d_visual;
+}
+
+HTTPPathRule::HTTPPathRule(const std::string& path)
+  :  d_path(path)
+{
+
+}
+
+bool HTTPPathRule::matches(const DNSQuestion* dq) const
+{
+  if(!dq->du) {
+    return false;
+  }
+
+  if(dq->du->req->query_at == SIZE_MAX) {
+    return dq->du->req->path.base == d_path;
+  }
+  else {
+    return d_path.compare(0, d_path.size(), dq->du->req->path.base, dq->du->req->query_at) == 0;
+  }
+}
+
+string HTTPPathRule::toString() const
+{
+  return "url path == " + d_path;
+}
+
+void dnsdistclient(int qsock, int rsock)
+{
+  for(;;) {
+    try {
+      DOHUnit* du = nullptr;
+      ssize_t got = recv(qsock, &du, sizeof(du), 0);
+      if (got < 0) {
+        warnlog("Error receving internal DoH query: %s", strerror(errno));
+        continue;
+      }
+      else if (static_cast<size_t>(got) < sizeof(du)) {
+        continue;
+      }
+
+      // if there was no EDNS, we add it with a large buffer size
+      // so we can use UDP to talk to the backend.
+      auto dh = const_cast<struct dnsheader*>(reinterpret_cast<const struct dnsheader*>(du->query.c_str()));
+
+      if(!dh->arcount) {
+        std::string res;
+        generateOptRR(std::string(), res, 4096, 0, false);
+
+        du->query += res;
+        dh = const_cast<struct dnsheader*>(reinterpret_cast<const struct dnsheader*>(du->query.c_str())); // may have reallocated
+        dh->arcount = htons(1);
+        du->ednsAdded = true;
+      }
+      else {
+        // we leave existing EDNS in place
+      }
+
+      if(processDOHQuery(du) < 0) {
+        du->error = true; // turns our drop into a 500
+        if(send(du->rsock, &du, sizeof(du), 0) != sizeof(du))
+          delete du;     // XXX but now what - will h2o time this out for us?
+      }
+    }
+    catch(const std::exception& e) {
+      errlog("Error while processing query received over DoH: %s", e.what());
+    }
+    catch(...) {
+      errlog("Unspecified error while processing query received over DoH");
+    }
+  }
+}
+
+// called if h2o finds that dnsdist gave us an answer
+static void on_dnsdist(h2o_socket_t *listener, const char *err)
+{
+  DOHUnit *du = nullptr;
+  DOHServerConfig* dsc = (DOHServerConfig*)listener->data;
+  ssize_t got = recv(dsc->dohresponsepair[1], &du, sizeof(du), 0);
+
+  if (got < 0) {
+    warnlog("Error reading a DOH internal response: %s", strerror(errno));
+    return;
+  }
+  else if (static_cast<size_t>(got) != sizeof(du)) {
+    return;
+  }
+
+  if(!du->req) { // it got killed in flight
+//    cout << "du "<<(void*)du<<" came back from dnsdist, but it was killed"<<endl;
+    delete du;
+    return;
+  }
+
+  *du->self = nullptr; // so we don't clean up again in on_generator_dispose
+  if(!du->error) {
+    ++dsc->df->d_validresponses;
+    du->req->res.status = 200;
+    du->req->res.reason = "OK";
+
+    h2o_add_header(&du->req->pool, &du->req->res.headers, H2O_TOKEN_CONTENT_TYPE, nullptr, H2O_STRLIT("application/dns-message"));
+
+    //    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;
+
+    du->req->res.content_length = du->query.size();
+    h2o_send_inline(du->req, du->query.c_str(), du->query.size());
+  }
+  else {
+    h2o_send_error_500(du->req, "Internal Server Error", "Internal Server Error", 0);
+    ++dsc->df->d_errorresponses;
+  }
+  delete du;
+}
+
+static void on_accept(h2o_socket_t *listener, const char *err)
+{
+  DOHServerConfig* dsc = (DOHServerConfig*)listener->data;
+  h2o_socket_t *sock = nullptr;
+
+  if (err != nullptr) {
+    return;
+  }
+  // do some dnsdist rules here to filter based on IP address
+  if ((sock = h2o_evloop_socket_accept(listener)) == nullptr)
+    return;
+
+  ComboAddress remote;
+
+  h2o_socket_getpeername(sock, reinterpret_cast<struct sockaddr*>(&remote));
+  //  cout<<"New HTTP accept for client "<<remote.toStringWithPort()<<": "<< listener->data << endl;
+
+  sock->data = dsc;
+  ++dsc->df->d_httpconnects;
+  h2o_accept(&dsc->h2o_accept_ctx, sock);
+}
+
+static int create_listener(const ComboAddress& addr, DOHServerConfig* dsc, int fd)
+{
+  auto sock = h2o_evloop_socket_create(dsc->h2o_ctx.loop, fd, H2O_SOCKET_FLAG_DONT_READ);
+  sock->data = (void*) dsc;
+  h2o_socket_read_start(sock, on_accept);
+
+  return 0;
+}
+
+static int setup_ssl(DOHServerConfig* dsc, const char *cert_file, const char *key_file, const char *ciphers)
+{
+  SSL_load_error_strings();
+  SSL_library_init();
+  OpenSSL_add_all_algorithms();
+
+  dsc->h2o_accept_ctx.ssl_ctx = SSL_CTX_new(SSLv23_server_method());
+
+  SSL_CTX_set_options(dsc->h2o_accept_ctx.ssl_ctx, SSL_OP_NO_SSLv2);
+
+#ifdef SSL_CTX_set_ecdh_auto
+  SSL_CTX_set_ecdh_auto(dsc->h2o_accept_ctx.ssl_ctx, 1);
+#endif
+
+  /* load certificate and private key */
+  if (SSL_CTX_use_certificate_chain_file(dsc->h2o_accept_ctx.ssl_ctx, cert_file) != 1) {
+    fprintf(stderr, "an error occurred while trying to load server certificate file:%s\n", cert_file);
+    return -1;
+  }
+  if (SSL_CTX_use_PrivateKey_file(dsc->h2o_accept_ctx.ssl_ctx, key_file, SSL_FILETYPE_PEM) != 1) {
+    fprintf(stderr, "an error occurred while trying to load private key file:%s\n", key_file);
+    return -1;
+  }
+
+  if (SSL_CTX_set_cipher_list(dsc->h2o_accept_ctx.ssl_ctx, ciphers) != 1) {
+    fprintf(stderr, "ciphers could not be set: %s\n", ciphers);
+    return -1;
+  }
+
+  h2o_ssl_register_alpn_protocols(dsc->h2o_accept_ctx.ssl_ctx, h2o_http2_alpn_protocols);
+
+  return 0;
+}
+
+void DOHFrontend::setup()
+{
+}
+
+// this is the entrypoint from dnsdist.cc
+void dohThread(ClientState* cs)
+try
+{
+  std::shared_ptr<DOHFrontend>& df = cs->dohFrontend;
+  auto dsc = new DOHServerConfig(cs);
+
+  std::thread dnsdistThread(dnsdistclient, dsc->dohquerypair[1], dsc->dohresponsepair[0]);
+  dnsdistThread.detach(); // gets us better error reporting
+
+  // I wonder if this registers an IP address.. I think it does
+  // this may mean we need to actually register a site "name" here and not the IP address
+  h2o_hostconf_t *hostconf = h2o_config_register_host(&dsc->h2o_config, h2o_iovec_init(df->d_local.toString().c_str(), df->d_local.toString().size()), 65535);
+
+  for(const auto& url : df->d_urls) {
+    register_handler(hostconf, url.c_str(), doh_handler);
+  }
+
+  h2o_context_init(&dsc->h2o_ctx, h2o_evloop_create(), &dsc->h2o_config);
+
+  // in this complicated way we insert the DOHServerConfig pointer in there
+  h2o_vector_reserve(nullptr, &dsc->h2o_ctx.storage, 1);
+  dsc->h2o_ctx.storage.entries[0].data = (void*)dsc;
+  ++dsc->h2o_ctx.storage.size;
+
+  auto sock = h2o_evloop_socket_create(dsc->h2o_ctx.loop, dsc->dohresponsepair[1], H2O_SOCKET_FLAG_DONT_READ);
+  sock->data = dsc;
+
+  // this listens to responses from dnsdist to turn into http responses
+  h2o_socket_read_start(sock, on_dnsdist);
+
+  // we should probably make that hash, algorithm etc line configurable too
+  if(setup_ssl(dsc, df->d_certFile.c_str(), df->d_keyFile.c_str(),
+                "DEFAULT:!MD5:!DSS:!DES:!RC4:!RC2:!SEED:!IDEA:!NULL:!ADH:!EXP:!SRP:!PSK") != 0)
+    throw std::runtime_error("Failed to setup SSL/TLS for DoH listener");
+
+  // as one does
+  dsc->h2o_accept_ctx.ctx = &dsc->h2o_ctx;
+  dsc->h2o_accept_ctx.hosts = dsc->h2o_config.hosts;
+
+  if (create_listener(df->d_local, dsc, cs->tcpFD) != 0) {
+    throw std::runtime_error("DOH server failed to listen on " + df->d_local.toStringWithPort() + ": " + strerror(errno));
+  }
+
+  while (h2o_evloop_run(dsc->h2o_ctx.loop, INT32_MAX) == 0)
+    ;
+ }
+ catch(const std::exception& e) {
+   throw runtime_error("DOH thread failed to launch: " + std::string(e.what()));
+ }
+ catch(...) {
+   throw runtime_error("DOH thread failed to launch");
+ }
diff --git a/pdns/dnsdistdist/doh.hh b/pdns/dnsdistdist/doh.hh
new file mode 100644 (file)
index 0000000..58d1ae5
--- /dev/null
@@ -0,0 +1,58 @@
+#pragma once
+#include "iputils.hh"
+
+struct DOHFrontend
+{
+  std::string d_certFile;
+  std::string d_keyFile;
+  ComboAddress d_local;
+
+  uint32_t d_idleTimeout{30};             // HTTP idle timeout in seconds
+  std::vector<std::string> d_urls;
+  std::string d_errortext;
+  std::atomic<uint64_t> d_httpconnects;   // number of TCP/IP connections established
+  std::atomic<uint64_t> d_http1queries;   // valid DNS queries received via HTTP1
+  std::atomic<uint64_t> d_http2queries;   // valid DNS queries received via HTTP2
+  std::atomic<uint64_t> d_tls10queries;   // valid DNS queries received via TLSv1.0
+  std::atomic<uint64_t> d_tls11queries;   // valid DNS queries received via TLSv1.1
+  std::atomic<uint64_t> d_tls12queries;   // valid DNS queries received via TLSv1.2
+  std::atomic<uint64_t> d_tls13queries;   // valid DNS queries received via TLSv1.3
+  std::atomic<uint64_t> d_tlsUnknownqueries;   // valid DNS queries received via unknown TLS version
+
+  std::atomic<uint64_t> d_getqueries;     // valid DNS queries received via GET
+  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_validresponses; // valid responses sent out
+
+#ifndef HAVE_DNS_OVER_HTTPS
+  void setup()
+  {
+  }
+#else
+  void setup();
+#endif /* HAVE_DNS_OVER_HTTPS */
+};
+
+#ifndef HAVE_DNS_OVER_HTTPS
+struct DOHUnit
+{
+};
+
+#else /* HAVE_DNS_OVER_HTTPS */
+struct st_h2o_req_t;
+
+struct DOHUnit
+{
+  std::string query;
+  ComboAddress remote;
+  ComboAddress dest;
+  st_h2o_req_t* req{nullptr};
+  DOHUnit** self{nullptr};
+  int rsock;
+  uint16_t qtype;
+  bool error{false};
+  bool ednsAdded{false};
+};
+
+#endif /* HAVE_DNS_OVER_HTTPS  */
diff --git a/pdns/dnsdistdist/m4/dnsdist_enable_doh.m4 b/pdns/dnsdistdist/m4/dnsdist_enable_doh.m4
new file mode 100644 (file)
index 0000000..4053d7a
--- /dev/null
@@ -0,0 +1,15 @@
+AC_DEFUN([DNSDIST_ENABLE_DNS_OVER_HTTPS], [
+  AC_MSG_CHECKING([whether to enable DNS over HTTPS (DoH) support])
+  AC_ARG_ENABLE([dns-over-https],
+    AS_HELP_STRING([--enable-dns-over-https], [enable DNS over HTTPS (DoH) support (requires libh2o) @<:@default=no@:>@]),
+    [enable_dns_over_https=$enableval],
+    [enable_dns_over_https=no]
+  )
+  AC_MSG_RESULT([$enable_dns_over_https])
+  AM_CONDITIONAL([HAVE_DNS_OVER_HTTPS], [test "x$enable_dns_over_https" != "xno"])
+
+  AM_COND_IF([HAVE_DNS_OVER_HTTPS], [
+    AC_DEFINE([HAVE_DNS_OVER_HTTPS], [1], [Define to 1 if you enable DNS over HTTPS support])
+  ])
+])
+
diff --git a/pdns/dnsdistdist/m4/pdns_check_libh2o_evloop.m4 b/pdns/dnsdistdist/m4/pdns_check_libh2o_evloop.m4
new file mode 100644 (file)
index 0000000..ffe066b
--- /dev/null
@@ -0,0 +1,8 @@
+AC_DEFUN([PDNS_CHECK_LIBH2OEVLOOP], [
+  HAVE_LIBH2OEVLOOP=0
+  PKG_CHECK_MODULES([LIBH2OEVLOOP], [libh2o-evloop], [
+    [HAVE_LIBH2OEVLOOP=1]
+    AC_DEFINE([HAVE_LIBH2OEVLOOP], [1], [Define to 1 if you have libh2o-evloop])
+  ], [ : ])
+  AM_CONDITIONAL([HAVE_LIBH2OEVLOOP], [test "x$LIBH2OEVLOOP_LIBS" != "x"])
+])