From fbf14b03c77c2c8e8ee3b814a76e705fa59d2318 Mon Sep 17 00:00:00 2001 From: Remi Gacogne Date: Mon, 15 Apr 2019 16:13:00 +0200 Subject: [PATCH] dnsdist: Add DNS over HTTPS support based on libh2o --- .../debian/dnsdist/debian-stretch/rules | 2 + .../dockerfiles/Dockerfile.debbuild-prepare | 9 + .../dockerfiles/Dockerfile.rpmbuild | 6 + builder-support/specs/dnsdist.spec | 4 +- pdns/dnsdist-carbon.cc | 33 + pdns/dnsdist-lua-rules.cc | 9 + pdns/dnsdist-lua.cc | 37 ++ pdns/dnsdist.cc | 68 +- pdns/dnsdist.hh | 16 +- pdns/dnsdistdist/Makefile.am | 24 + pdns/dnsdistdist/configure.ac | 35 +- pdns/dnsdistdist/dnsdist-rules.hh | 23 + pdns/dnsdistdist/doh.cc | 603 ++++++++++++++++++ pdns/dnsdistdist/doh.hh | 58 ++ pdns/dnsdistdist/m4/dnsdist_enable_doh.m4 | 15 + .../m4/pdns_check_libh2o_evloop.m4 | 8 + 16 files changed, 925 insertions(+), 25 deletions(-) create mode 100644 pdns/dnsdistdist/doh.cc create mode 100644 pdns/dnsdistdist/doh.hh create mode 100644 pdns/dnsdistdist/m4/dnsdist_enable_doh.m4 create mode 100644 pdns/dnsdistdist/m4/pdns_check_libh2o_evloop.m4 diff --git a/builder-support/debian/dnsdist/debian-stretch/rules b/builder-support/debian/dnsdist/debian-stretch/rules index f5a19a283..9d870ed19 100755 --- a/builder-support/debian/dnsdist/debian-stretch/rules +++ b/builder-support/debian/dnsdist/debian-stretch/rules @@ -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 \ diff --git a/builder-support/dockerfiles/Dockerfile.debbuild-prepare b/builder-support/dockerfiles/Dockerfile.debbuild-prepare index f466341df..22a27b06d 100644 --- a/builder-support/dockerfiles/Dockerfile.debbuild-prepare +++ b/builder-support/dockerfiles/Dockerfile.debbuild-prepare @@ -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 diff --git a/builder-support/dockerfiles/Dockerfile.rpmbuild b/builder-support/dockerfiles/Dockerfile.rpmbuild index 91a2bd57b..3d8ac4a7e 100644 --- a/builder-support/dockerfiles/Dockerfile.rpmbuild +++ b/builder-support/dockerfiles/Dockerfile.rpmbuild @@ -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 diff --git a/builder-support/specs/dnsdist.spec b/builder-support/specs/dnsdist.spec index 1aa93848a..cb476c49b 100644 --- a/builder-support/specs/dnsdist.spec +++ b/builder-support/specs/dnsdist.spec @@ -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} diff --git a/pdns/dnsdist-carbon.cc b/pdns/dnsdist-carbon.cc index 1fa49f584..14b72e048 100644 --- a/pdns/dnsdist-carbon.cc +++ b/pdns/dnsdist-carbon.cc @@ -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&>> 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<(new RegexRule(str)); }); +#ifdef HAVE_DNS_OVER_HTTPS + g_lua.writeFunction("HTTPHeaderRule", [](const std::string& header, const std::string& regex) { + return std::shared_ptr(new HTTPHeaderRule(header, regex)); + }); + g_lua.writeFunction("HTTPPathRule", [](const std::string& path) { + return std::shared_ptr(new HTTPPathRule(path)); + }); +#endif + #ifdef HAVE_RE2 g_lua.writeFunction("RE2Rule", [](const std::string& str) { return std::shared_ptr(new RE2Rule(str)); diff --git a/pdns/dnsdist-lua.cc b/pdns/dnsdist-lua.cc index ad4c55472..bb68d91cb 100644 --- a/pdns/dnsdist-lua.cc +++ b/pdns/dnsdist-lua.cc @@ -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 > > urls, boost::optional 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(); + 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((*vars)["idleTimeout"]); + } + } + g_dohlocals.push_back(frontend); + auto cs = std::unique_ptr(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>> certFiles, boost::variant>> keyFiles, boost::optional vars) { if (client) return; diff --git a/pdns/dnsdist.cc b/pdns/dnsdist.cc index f7bfc1dee..28a9d5fc5 100644 --- a/pdns/dnsdist.cc +++ b/pdns/dnsdist.cc @@ -93,6 +93,7 @@ GlobalStateHolder g_ACL; string g_outputBuffer; std::vector> g_tlslocals; +std::vector> g_dohlocals; std::vector> g_dnsCryptLocals; #ifdef HAVE_EBPF shared_ptr g_defaultBPFFilter; @@ -486,7 +487,7 @@ static bool sendUDPResponse(int origFD, const char* response, const uint16_t res } -static int pickBackendSocketForSending(std::shared_ptr& state) +int pickBackendSocketForSending(std::shared_ptr& state) { return state->sockets[state->socketsOffset++ % state->sockets.size()]; } @@ -537,13 +538,14 @@ try { uint16_t responseLen = static_cast(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& ss, const int sd, const char* request, const size_t requestLen, bool healthCheck=false) +ssize_t udpClientSendRequestToBackend(const std::shared_ptr& 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& 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& 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()) { diff --git a/pdns/dnsdist.hh b/pdns/dnsdist.hh index 71651495f..3d32ee2a1 100644 --- a/pdns/dnsdist.hh +++ b/pdns/dnsdist.hh @@ -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 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 packetCache{nullptr}; std::shared_ptr 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 dnscryptCtx{nullptr}; std::shared_ptr tlsFrontend{nullptr}; + std::shared_ptr dohFrontend{nullptr}; std::string interface; std::atomic queries{0}; std::atomic 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>> g_locals; // not changed at runtime (we hope XXX) extern std::vector> g_tlslocals; +extern std::vector> g_dohlocals; extern std::vector> g_frontends; extern bool g_truncateTC; extern bool g_fixupCase; @@ -1103,6 +1111,9 @@ void setWebserverCustomHeaders(const boost::optional& state); +ssize_t udpClientSendRequestToBackend(const std::shared_ptr& ss, const int sd, const char* request, const size_t requestLen, bool healthCheck=false); diff --git a/pdns/dnsdistdist/Makefile.am b/pdns/dnsdistdist/Makefile.am index 3c4386814..f15cf5af4 100644 --- a/pdns/dnsdistdist/Makefile.am +++ b/pdns/dnsdistdist/Makefile.am @@ -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 diff --git a/pdns/dnsdistdist/configure.ac b/pdns/dnsdistdist/configure.ac index 7d8014c95..456879f33 100644 --- a/pdns/dnsdistdist/configure.ac +++ b/pdns/dnsdistdist/configure.ac @@ -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([]) diff --git a/pdns/dnsdistdist/dnsdist-rules.hh b/pdns/dnsdistdist/dnsdist-rules.hh index bbc4ff205..a25d8572c 100644 --- a/pdns/dnsdistdist/dnsdist-rules.hh +++ b/pdns/dnsdistdist/dnsdist-rules.hh @@ -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 index 000000000..c3a740989 --- /dev/null +++ b/pdns/dnsdistdist/doh.cc @@ -0,0 +1,603 @@ +#define H2O_USE_EPOLL 1 +#include +#include +#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 +#include "dns.hh" +#include "dolog.hh" +#include "dnsdist-ecs.hh" +#include "dnsdist-rules.hh" +#include "dnsdist-xpf.hh" +#include + +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 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(du->query.c_str()); + struct dnsheader* dh = reinterpret_cast(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 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(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"<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(new DOHUnit); + du->self = reinterpret_cast(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"<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(&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(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(reinterpret_cast(du->query.c_str())); + + if(!dh->arcount) { + std::string res; + generateOptRR(std::string(), res, 4096, 0, false); + + du->query += res; + dh = const_cast(reinterpret_cast(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(got) != sizeof(du)) { + return; + } + + if(!du->req) { // it got killed in flight +// cout << "du "<<(void*)du<<" came back from dnsdist, but it was killed"<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 "<query.size()<<" bytes over https, TC="<tc<<", RCODE="<rcode<<", qtype="<qtype<<", req="<<(void*)du->req<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(&remote)); + // cout<<"New HTTP accept for client "<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& 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 index 000000000..58d1ae56b --- /dev/null +++ b/pdns/dnsdistdist/doh.hh @@ -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 d_urls; + std::string d_errortext; + std::atomic d_httpconnects; // number of TCP/IP connections established + std::atomic d_http1queries; // valid DNS queries received via HTTP1 + std::atomic d_http2queries; // valid DNS queries received via HTTP2 + std::atomic d_tls10queries; // valid DNS queries received via TLSv1.0 + std::atomic d_tls11queries; // valid DNS queries received via TLSv1.1 + std::atomic d_tls12queries; // valid DNS queries received via TLSv1.2 + std::atomic d_tls13queries; // valid DNS queries received via TLSv1.3 + std::atomic d_tlsUnknownqueries; // valid DNS queries received via unknown TLS version + + std::atomic d_getqueries; // valid DNS queries received via GET + std::atomic d_postqueries; // valid DNS queries received via POST + std::atomic d_badrequests; // request could not be converted to dns query + std::atomic d_errorresponses; // dnsdist set 'error' on response + std::atomic 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 index 000000000..4053d7a09 --- /dev/null +++ b/pdns/dnsdistdist/m4/dnsdist_enable_doh.m4 @@ -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 index 000000000..ffe066b72 --- /dev/null +++ b/pdns/dnsdistdist/m4/pdns_check_libh2o_evloop.m4 @@ -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"]) +]) -- 2.40.0