From: Remi Gacogne Date: Tue, 31 Aug 2021 15:16:09 +0000 (+0200) Subject: dnsdist: Add unit tests for outgoing DoH X-Git-Tag: dnsdist-1.7.0-alpha1~23^2~19 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=f05cd66c2a4d840cf66ade936be1ae0a9b846201;p=thirdparty%2Fpdns.git dnsdist: Add unit tests for outgoing DoH --- diff --git a/pdns/dnsdist-lua.cc b/pdns/dnsdist-lua.cc index 3042c8b597..922e5e5786 100644 --- a/pdns/dnsdist-lua.cc +++ b/pdns/dnsdist-lua.cc @@ -389,7 +389,7 @@ static void setupLuaConfig(LuaContext& luaCtx, bool client, bool configCheck) } if(vars.count("retries")) { - ret->retries=std::stoi(boost::get(vars["retries"])); + ret->d_retries = std::stoi(boost::get(vars["retries"])); } if(vars.count("checkInterval")) { diff --git a/pdns/dnsdist-tcp.cc b/pdns/dnsdist-tcp.cc index 71dac2f47c..f839b321f2 100644 --- a/pdns/dnsdist-tcp.cc +++ b/pdns/dnsdist-tcp.cc @@ -695,6 +695,8 @@ static void handleQuery(std::shared_ptr& state, cons ++state->d_currentQueriesCount; if (ds->isDoH()) { + vinfolog("Got query for %s|%s from %s (%s, %d bytes), relayed to %s", ids.qname.toLogString(), QType(ids.qtype).toString(), state->d_proxiedRemote.toStringWithPort(), (state->d_handler.isTLS() ? "DoT" : "TCP"), state->d_buffer.size(), ds->getName()); + auto incoming = std::make_shared(state, state->d_threadData.crossProtocolResponsesPipe); auto cpq = std::make_unique(std::move(state->d_buffer), std::move(ids), ds, incoming); @@ -1175,14 +1177,19 @@ static void handleCrossProtocolResponse(int pipefd, FDMultiplexer::funcparam_t& delete tmp; tmp = nullptr; - if (response.d_response.d_buffer.empty()) { - response.d_state->notifyIOError(std::move(response.d_response.d_idstate), response.d_now); - } - else if (response.d_response.d_idstate.qtype == QType::AXFR || response.d_response.d_idstate.qtype == QType::IXFR) { - response.d_state->handleXFRResponse(response.d_now, std::move(response.d_response)); + try { + if (response.d_response.d_buffer.empty()) { + response.d_state->notifyIOError(std::move(response.d_response.d_idstate), response.d_now); + } + else if (response.d_response.d_idstate.qtype == QType::AXFR || response.d_response.d_idstate.qtype == QType::IXFR) { + response.d_state->handleXFRResponse(response.d_now, std::move(response.d_response)); + } + else { + response.d_state->handleResponse(response.d_now, std::move(response.d_response)); + } } - else { - response.d_state->handleXFRResponse(response.d_now, std::move(response.d_response)); + catch (...) { + /* no point bubbling up from there */ } } diff --git a/pdns/dnsdist.hh b/pdns/dnsdist.hh index aca9610530..31c62c7236 100644 --- a/pdns/dnsdist.hh +++ b/pdns/dnsdist.hh @@ -730,7 +730,7 @@ struct DownstreamState unsigned int checkInterval{1}; unsigned int lastCheck{0}; const unsigned int sourceItf{0}; - uint16_t retries{5}; + uint16_t d_retries{5}; uint16_t xpfRRCode{0}; uint16_t checkTimeout{1000}; /* in milliseconds */ uint8_t currentCheckFailures{0}; diff --git a/pdns/dnsdistdist/Makefile.am b/pdns/dnsdistdist/Makefile.am index 3e62c1ff18..4419c422fc 100644 --- a/pdns/dnsdistdist/Makefile.am +++ b/pdns/dnsdistdist/Makefile.am @@ -286,6 +286,7 @@ testrunner_SOURCES = \ test-dnsdistdynblocks_hh.cc \ test-dnsdistkvs_cc.cc \ test-dnsdistlbpolicies_cc.cc \ + test-dnsdistnghttp2_cc.cc \ test-dnsdistpacketcache_cc.cc \ test-dnsdistrings_cc.cc \ test-dnsdistrules_cc.cc \ diff --git a/pdns/dnsdistdist/dnsdist-healthchecks.cc b/pdns/dnsdistdist/dnsdist-healthchecks.cc index 7ae87d9c19..f452932d65 100644 --- a/pdns/dnsdistdist/dnsdist-healthchecks.cc +++ b/pdns/dnsdistdist/dnsdist-healthchecks.cc @@ -431,7 +431,7 @@ bool queueHealthCheck(std::unique_ptr& mplexer, const std::shared } return false; } - catch(...) + catch (...) { if (g_verboseHealthChecks) { infolog("Unknown exception while checking the health of backend %s", ds->getNameWithAddr()); @@ -451,8 +451,15 @@ void handleQueuedHealthChecks(FDMultiplexer& mplexer, bool initial) } break; } + + handleH2Timeouts(mplexer, now); + auto timeouts = mplexer.getTimeouts(now); for (const auto& timeout : timeouts) { + if (timeout.second.type() != typeid(std::shared_ptr)) { + continue; + } + auto data = boost::any_cast>(timeout.second); try { if (data->d_ioState) { @@ -481,6 +488,9 @@ void handleQueuedHealthChecks(FDMultiplexer& mplexer, bool initial) timeouts = mplexer.getTimeouts(now, true); for (const auto& timeout : timeouts) { + if (timeout.second.type() != typeid(std::shared_ptr)) { + continue; + } auto data = boost::any_cast>(timeout.second); try { data->d_ioState.reset(); diff --git a/pdns/dnsdistdist/dnsdist-nghttp2.cc b/pdns/dnsdistdist/dnsdist-nghttp2.cc index 2b503b95df..f99d6870d3 100644 --- a/pdns/dnsdistdist/dnsdist-nghttp2.cc +++ b/pdns/dnsdistdist/dnsdist-nghttp2.cc @@ -84,6 +84,7 @@ private: std::shared_ptr d_sender{nullptr}; TCPQuery d_query; PacketBuffer d_buffer; + size_t d_queryPos{0}; uint16_t d_responseCode{0}; bool d_finished{false}; }; @@ -92,6 +93,7 @@ private: void stopIO(); void handleResponse(PendingRequest&& request); void handleResponseError(PendingRequest&& request, const struct timeval& now); + void handleIOError(); uint32_t getConcurrentStreamsCount() const; size_t getUsageCount() const @@ -106,7 +108,6 @@ private: std::unordered_map d_currentStreams; PacketBuffer d_out; PacketBuffer d_in; - size_t d_queryPos{0}; size_t d_outPos{0}; size_t d_inPos{0}; uint32_t d_highestStreamID{0}; @@ -116,8 +117,9 @@ private: class DownstreamDoHConnectionsManager { public: - static std::shared_ptr getConnectionToDownstream(std::unique_ptr& mplexer, std::shared_ptr& ds, const struct timeval& now); + static std::shared_ptr getConnectionToDownstream(std::unique_ptr& mplexer, const std::shared_ptr& ds, const struct timeval& now); static void releaseDownstreamConnection(std::shared_ptr&& conn); + static bool removeDownstreamConnection(std::shared_ptr& conn); static void cleanupClosedConnections(struct timeval now); static size_t clear(); @@ -147,21 +149,42 @@ void DoHConnectionToBackend::handleResponse(PendingRequest&& request) { struct timeval now; gettimeofday(&now, nullptr); - request.d_sender->handleResponse(now, TCPResponse(std::move(request.d_buffer), std::move(request.d_query.d_idstate), shared_from_this())); + try { + request.d_sender->handleResponse(now, TCPResponse(std::move(request.d_buffer), std::move(request.d_query.d_idstate), shared_from_this())); + } + catch (const std::exception& e) { + vinfolog("Got exception while handling response for cross-protocol DoH: %s", e.what()); + } } void DoHConnectionToBackend::handleResponseError(PendingRequest&& request, const struct timeval& now) { - request.d_sender->notifyIOError(std::move(request.d_query.d_idstate), now); + try { + request.d_sender->notifyIOError(std::move(request.d_query.d_idstate), now); + } + catch (const std::exception& e) { + vinfolog("Got exception while handling response for cross-protocol DoH: %s", e.what()); + } } -void DoHConnectionToBackend::handleTimeout(const struct timeval& now, bool write) +void DoHConnectionToBackend::handleIOError() { d_connectionDied = true; + nghttp2_session_terminate_session(d_session.get(), NGHTTP2_PROTOCOL_ERROR); + + struct timeval now; + gettimeofday(&now, nullptr); for (auto& request : d_currentStreams) { handleResponseError(std::move(request.second), now); } + d_currentStreams.clear(); + stopIO(); +} + +void DoHConnectionToBackend::handleTimeout(const struct timeval& now, bool write) +{ + handleIOError(); } bool DoHConnectionToBackend::canBeReused() const @@ -182,18 +205,6 @@ bool DoHConnectionToBackend::canBeReused() const return true; } -#define MAKE_NV(NAME, VALUE, VALUELEN) \ - { \ - (uint8_t*)NAME, (uint8_t*)VALUE, sizeof(NAME) - 1, VALUELEN, \ - NGHTTP2_NV_FLAG_NONE \ - } - -#define MAKE_NV2(NAME, VALUE) \ - { \ - (uint8_t*)NAME, (uint8_t*)VALUE, sizeof(NAME) - 1, sizeof(VALUE) - 1, \ - NGHTTP2_NV_FLAG_NONE \ - } - const std::unordered_map DoHConnectionToBackend::s_constants = { {"method-name", ":method"}, {"method-value", "POST"}, @@ -235,9 +246,8 @@ void DoHConnectionToBackend::addDynamicHeader(std::vector& headers, void DoHConnectionToBackend::queueQuery(std::shared_ptr& sender, TCPQuery&& query) { + // cerr<<"in "<<__PRETTY_FUNCTION__<<" with query ID "<id)<d_addXForwardedHeaders; @@ -259,24 +269,24 @@ void DoHConnectionToBackend::queueQuery(std::shared_ptr& sender, addStaticHeader(headers, "user-agent-name", "user-agent-value"); addDynamicHeader(headers, "content-length-name", payloadSize); /* no need to add these headers for health-check queries */ - if (addXForwarded && d_currentQuery.d_idstate.origRemote.getPort() != 0) { - remote = d_currentQuery.d_idstate.origRemote.toString(); - remotePort = std::to_string(d_currentQuery.d_idstate.origRemote.getPort()); + if (addXForwarded && query.d_idstate.origRemote.getPort() != 0) { + remote = query.d_idstate.origRemote.toString(); + remotePort = std::to_string(query.d_idstate.origRemote.getPort()); addDynamicHeader(headers, "x-forwarded-for-name", remote); addDynamicHeader(headers, "x-forwarded-port-name", remotePort); - if (d_currentQuery.d_idstate.cs != nullptr) { - if (d_currentQuery.d_idstate.cs->isUDP()) { + if (query.d_idstate.cs != nullptr) { + if (query.d_idstate.cs->isUDP()) { addStaticHeader(headers, "x-forwarded-proto-name", "x-forwarded-proto-value-dns-over-udp"); } - else if (d_currentQuery.d_idstate.cs->isDoH()) { - if (d_currentQuery.d_idstate.cs->hasTLS()) { + else if (query.d_idstate.cs->isDoH()) { + if (query.d_idstate.cs->hasTLS()) { addStaticHeader(headers, "x-forwarded-proto-name", "x-forwarded-proto-value-dns-over-https"); } else { addStaticHeader(headers, "x-forwarded-proto-name", "x-forwarded-proto-value-dns-over-http"); } } - else if (d_currentQuery.d_idstate.cs->hasTLS()) { + else if (query.d_idstate.cs->hasTLS()) { addStaticHeader(headers, "x-forwarded-proto-name", "x-forwarded-proto-value-dns-over-tls"); } else { @@ -285,50 +295,58 @@ void DoHConnectionToBackend::queueQuery(std::shared_ptr& sender, } } + PendingRequest pending; + pending.d_query = std::move(query); + pending.d_sender = std::move(sender); + + uint32_t streamId = nghttp2_session_get_next_stream_id(d_session.get()); + auto insertPair = d_currentStreams.insert({streamId, std::move(pending)}); + if (!insertPair.second) { + /* there is a stream ID collision, something is very wrong! */ + d_connectionDied = true; + nghttp2_session_terminate_session(d_session.get(), NGHTTP2_NO_ERROR); + throw std::runtime_error("Stream ID collision"); + } + /* if data_prd is not NULL, it provides data which will be sent in subsequent DATA frames. In this case, a method that allows request message bodies (https://tools.ietf.org/html/rfc7231#section-4) must be specified with :method key (e.g. POST). This function does not take ownership of the data_prd. The function copies the members of the data_prd. If data_prd is NULL, HEADERS have END_STREAM set. */ nghttp2_data_provider data_provider; + + /* we will not use this pointer */ data_provider.source.ptr = this; data_provider.read_callback = [](nghttp2_session* session, int32_t stream_id, uint8_t* buf, size_t length, uint32_t* data_flags, nghttp2_data_source* source, void* user_data) -> ssize_t { - auto userData = reinterpret_cast(user_data); + auto conn = reinterpret_cast(user_data); + auto& request = conn->d_currentStreams.at(stream_id); size_t toCopy = 0; - if (userData->d_queryPos < userData->d_currentQuery.d_buffer.size()) { - size_t remaining = userData->d_currentQuery.d_buffer.size() - userData->d_queryPos; + if (request.d_queryPos < request.d_query.d_buffer.size()) { + size_t remaining = request.d_query.d_buffer.size() - request.d_queryPos; toCopy = length > remaining ? remaining : length; - memcpy(buf, &userData->d_currentQuery.d_buffer.at(userData->d_queryPos), toCopy); - userData->d_queryPos += toCopy; + memcpy(buf, &request.d_query.d_buffer.at(request.d_queryPos), toCopy); + request.d_queryPos += toCopy; } - if (userData->d_queryPos >= userData->d_currentQuery.d_buffer.size()) { + if (request.d_queryPos >= request.d_query.d_buffer.size()) { *data_flags |= NGHTTP2_DATA_FLAG_EOF; } return toCopy; }; - auto stream_id = nghttp2_submit_request(d_session.get(), nullptr, headers.data(), headers.size(), &data_provider, this); - if (stream_id < 0) { + auto newStreamId = nghttp2_submit_request(d_session.get(), nullptr, headers.data(), headers.size(), &data_provider, this); + if (newStreamId < 0) { d_connectionDied = true; - throw std::runtime_error("Error submitting HTTP request:" + std::string(nghttp2_strerror(stream_id))); + d_currentStreams.erase(streamId); + throw std::runtime_error("Error submitting HTTP request:" + std::string(nghttp2_strerror(newStreamId))); } - //cerr<<"stream ID is "<d_inPos = 0; conn->d_in.resize(conn->d_in.size() + 512); - //cerr<<"trying to read "<d_in.size()<d_in.size()<d_handler->tryRead(conn->d_in, conn->d_inPos, conn->d_in.size(), true); - //cerr<<"got a "<<(int)newState<<" state and "<d_inPos<<" bytes"<d_inPos<<" bytes"<d_in.resize(conn->d_inPos); - if (newState == IOState::Done) { + + if (conn->d_inPos > 0) { + /* we got something */ auto readlen = nghttp2_session_mem_recv(conn->d_session.get(), conn->d_in.data(), conn->d_inPos); - //cerr<<"nghttp2_session_mem_recv returned "< 0 && static_cast(readlen) < conn->d_inPos) { - cerr << "Fatal error: " << nghttp2_strerror((int)readlen) << endl; - return; + throw std::runtime_error("Fatal error while passing received data to nghttp2: " + std::string(nghttp2_strerror((int)readlen))); } + // cerr<<"after read send"<d_session.get()); + } + + if (newState == IOState::Done) { if (conn->getConcurrentStreamsCount() == 0) { conn->stopIO(); ioGuard.release(); @@ -380,6 +403,7 @@ void DoHConnectionToBackend::handleReadableIOCallback(int fd, FDMultiplexer::fun } else { if (newState == IOState::NeedWrite) { + // cerr<<"need write"<updateIO(IOState::NeedWrite, handleReadableIOCallback); } ioGuard.release(); @@ -387,7 +411,8 @@ void DoHConnectionToBackend::handleReadableIOCallback(int fd, FDMultiplexer::fun } } catch (const std::exception& e) { - cerr << "Exception while trying to read from HTTP backend connection: " << e.what() << endl; + vinfolog("Exception while trying to read from HTTP backend connection: %s", e.what()); + conn->handleIOError(); break; } } while (conn->getConcurrentStreamsCount() > 0); @@ -401,30 +426,41 @@ void DoHConnectionToBackend::handleWritableIOCallback(int fd, FDMultiplexer::fun } IOStateGuard ioGuard(conn->d_ioState); - //cerr<<"trying to write "<d_out.size()-conn->d_outPos<d_out.size()-conn->d_outPos<d_handler->tryWrite(conn->d_out, conn->d_outPos, conn->d_out.size()); - //cerr<<"got a "<<(int)newState<<" state, "<d_out.size()-conn->d_outPos<<" bytes remaining"<d_out.size()-conn->d_outPos<<" bytes remaining"<updateIO(IOState::NeedRead, handleWritableIOCallback); } else if (newState == IOState::Done) { + // cerr<<"done, buffer size was "<d_out.size()<<", pos was "<d_outPos<d_queries; conn->d_out.clear(); conn->d_outPos = 0; conn->stopIO(); - conn->updateIO(IOState::NeedRead, handleReadableIOCallback); + if (conn->getConcurrentStreamsCount() > 0) { + conn->updateIO(IOState::NeedRead, handleReadableIOCallback); + } } ioGuard.release(); } catch (const std::exception& e) { - cerr << "Exception while trying to write (ready) to HTTP backend connection: " << e.what() << endl; + vinfolog("Exception while trying to write (ready) to HTTP backend connection: %s", e.what()); + conn->handleIOError(); } } void DoHConnectionToBackend::stopIO() { d_ioState->reset(); + + if (d_connectionDied) { + /* remove ourselves from the connection cache, this might mean that our + reference count drops to zero after that, so we need to be careful */ + auto shared = std::dynamic_pointer_cast(shared_from_this()); + DownstreamDoHConnectionsManager::removeDownstreamConnection(shared); + } } void DoHConnectionToBackend::updateIO(IOState newState, FDMultiplexer::callbackfunc_t callback) @@ -492,19 +528,25 @@ ssize_t DoHConnectionToBackend::send_callback(nghttp2_session* session, const ui if (bufferWasEmpty) { try { + // cerr<<"in "<<__PRETTY_FUNCTION__<<" trying to write "<d_out.size()-conn->d_outPos<d_handler->tryWrite(conn->d_out, conn->d_outPos, conn->d_out.size()); + // cerr<<"got a "<<(int)state<<" state, "<d_out.size()-conn->d_outPos<<" bytes remaining"<d_queries; conn->d_out.clear(); conn->d_outPos = 0; - conn->addToIOState(IOState::NeedRead, handleReadableIOCallback); + conn->stopIO(); + if (conn->getConcurrentStreamsCount() > 0) { + conn->updateIO(IOState::NeedRead, handleReadableIOCallback); + } } else { conn->updateIO(state, handleWritableIOCallback); } } catch (const std::exception& e) { - cerr << "Exception while trying to write (send) to HTTP backend connection: " << e.what() << endl; + vinfolog("Exception while trying to write (send) to HTTP backend connection: %s", e.what()); + conn->handleIOError(); } } @@ -514,7 +556,7 @@ ssize_t DoHConnectionToBackend::send_callback(nghttp2_session* session, const ui int DoHConnectionToBackend::on_frame_recv_callback(nghttp2_session* session, const nghttp2_frame* frame, void* user_data) { DoHConnectionToBackend* conn = reinterpret_cast(user_data); - //cerr<<"Frame type is "<hd.type)<hd.type)<hd.type) { case NGHTTP2_HEADERS: @@ -543,7 +585,7 @@ int DoHConnectionToBackend::on_frame_recv_callback(nghttp2_session* session, con if ((frame->hd.type == NGHTTP2_HEADERS || frame->hd.type == NGHTTP2_DATA) && frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { auto stream = conn->d_currentStreams.find(frame->hd.stream_id); if (stream != conn->d_currentStreams.end()) { - //cerr<<"Stream "<hd.stream_id<<" is now finished"<hd.stream_id<<" is now finished"<second.d_finished = true; auto request = std::move(stream->second); @@ -575,7 +617,7 @@ int DoHConnectionToBackend::on_frame_recv_callback(nghttp2_session* session, con int DoHConnectionToBackend::on_data_chunk_recv_callback(nghttp2_session* session, uint8_t flags, int32_t stream_id, const uint8_t* data, size_t len, void* user_data) { DoHConnectionToBackend* conn = reinterpret_cast(user_data); - //cerr<<"Got data of size "<d_currentStreams.find(stream_id); if (stream == conn->d_currentStreams.end()) { vinfolog("Unable to match the stream ID %d to a known one!", stream_id); @@ -624,13 +666,12 @@ int DoHConnectionToBackend::on_stream_close_callback(nghttp2_session* session, i return 0; } - cerr << "Stream " << stream_id << " closed with error_code=" << error_code << endl; + // cerr << "Stream " << stream_id << " closed with error_code=" << error_code << endl; conn->d_connectionDied = true; auto stream = conn->d_currentStreams.find(stream_id); if (stream == conn->d_currentStreams.end()) { /* we don't care, then */ - cerr << "we don't care" << endl; return 0; } @@ -639,9 +680,16 @@ int DoHConnectionToBackend::on_stream_close_callback(nghttp2_session* session, i auto request = std::move(stream->second); conn->d_currentStreams.erase(stream->first); - //cerr<<"in "<<__PRETTY_FUNCTION__<<", looking for a connection to send a query of size "<d_mplexer, conn->d_ds, now); - downstream->queueQuery(request.d_sender, std::move(request.d_query)); + // cerr<<"Query has "<d_ds->d_retries<d_ds->d_retries) { + // cerr<<"in "<<__PRETTY_FUNCTION__<<", looking for a connection to send a query of size "<d_mplexer, conn->d_ds, now); + downstream->queueQuery(request.d_sender, std::move(request.d_query)); + } + else { + conn->handleResponseError(std::move(request), now); + } //cerr<<"we now have "<getConcurrentStreamsCount()<<" concurrent connections"<getConcurrentStreamsCount() == 0) { @@ -755,6 +803,35 @@ size_t DownstreamDoHConnectionsManager::s_maxCachedConnectionsPerDownstream{10}; time_t DownstreamDoHConnectionsManager::s_nextCleanup{0}; uint16_t DownstreamDoHConnectionsManager::s_cleanupInterval{60}; +size_t DownstreamDoHConnectionsManager::clear() +{ + size_t result = 0; + for (const auto& backend : t_downstreamConnections) { + result += backend.second.size(); + } + t_downstreamConnections.clear(); + return result; +} + +bool DownstreamDoHConnectionsManager::removeDownstreamConnection(std::shared_ptr& conn) +{ + bool found = false; + auto backendIt = t_downstreamConnections.find(conn->getDS()->getID()); + if (backendIt == t_downstreamConnections.end()) { + return found; + } + + for (auto connIt = backendIt->second.begin(); connIt != backendIt->second.end(); ++connIt) { + if (*connIt == conn) { + backendIt->second.erase(connIt); + found = true; + break; + } + } + + return found; +} + void DownstreamDoHConnectionsManager::cleanupClosedConnections(struct timeval now) { struct timeval freshCutOff = now; @@ -790,7 +867,7 @@ void DownstreamDoHConnectionsManager::cleanupClosedConnections(struct timeval no } } -std::shared_ptr DownstreamDoHConnectionsManager::getConnectionToDownstream(std::unique_ptr& mplexer, std::shared_ptr& ds, const struct timeval& now) +std::shared_ptr DownstreamDoHConnectionsManager::getConnectionToDownstream(std::unique_ptr& mplexer, const std::shared_ptr& ds, const struct timeval& now) { std::shared_ptr result; struct timeval freshCutOff = now; @@ -902,23 +979,8 @@ static void dohClientThread(int crossProtocolPipeFD) if (now.tv_sec > lastTimeoutScan) { lastTimeoutScan = now.tv_sec; - auto expiredReadConns = data.mplexer->getTimeouts(now, false); - for (const auto& cbData : expiredReadConns) { - if (cbData.second.type() == typeid(std::shared_ptr)) { - auto conn = boost::any_cast>(cbData.second); - vinfolog("Timeout (read) from remote DoH backend %s", conn->getBackendName()); - conn->handleTimeout(now, false); - } - } - auto expiredWriteConns = data.mplexer->getTimeouts(now, true); - for (const auto& cbData : expiredWriteConns) { - if (cbData.second.type() == typeid(std::shared_ptr)) { - auto conn = boost::any_cast>(cbData.second); - vinfolog("Timeout (write) from remote DoH backend %s", conn->getBackendName()); - conn->handleTimeout(now, true); - } - } + handleH2Timeouts(*data.mplexer, now); if (g_dohStatesDumpRequested > 0) { /* just to keep things clean in the output, debug only */ @@ -1134,11 +1196,55 @@ bool sendH2Query(const std::shared_ptr& ds, std::unique_ptr(ds, mplexer, now); - newConnection->setHealthCheck(healthCheck); - newConnection->queueQuery(sender, std::move(query)); + if (healthCheck) { + /* always do health-checks over a new connection */ + auto newConnection = std::make_shared(ds, mplexer, now); + newConnection->setHealthCheck(healthCheck); + newConnection->queueQuery(sender, std::move(query)); + } + else { + auto connection = DownstreamDoHConnectionsManager::getConnectionToDownstream(mplexer, ds, now); + connection->queueQuery(sender, std::move(query)); + } + return true; #else /* HAVE_NGHTTP2 */ return false; #endif /* HAVE_NGHTTP2 */ } + +size_t clearH2Connections() +{ + size_t cleared = 0; +#ifdef HAVE_NGHTTP2 + cleared = DownstreamDoHConnectionsManager::clear(); +#endif /* HAVE_NGHTTP2 */ + return cleared; +} + +size_t handleH2Timeouts(FDMultiplexer& mplexer, const struct timeval& now) +{ + size_t got = 0; +#ifdef HAVE_NGHTTP2 + auto expiredReadConns = mplexer.getTimeouts(now, false); + for (const auto& cbData : expiredReadConns) { + if (cbData.second.type() == typeid(std::shared_ptr)) { + auto conn = boost::any_cast>(cbData.second); + vinfolog("Timeout (read) from remote DoH backend %s", conn->getBackendName()); + conn->handleTimeout(now, false); + ++got; + } + } + + auto expiredWriteConns = mplexer.getTimeouts(now, true); + for (const auto& cbData : expiredWriteConns) { + if (cbData.second.type() == typeid(std::shared_ptr)) { + auto conn = boost::any_cast>(cbData.second); + vinfolog("Timeout (write) from remote DoH backend %s", conn->getBackendName()); + conn->handleTimeout(now, true); + ++got; + } + } +#endif /* HAVE_NGHTTP2 */ + return got; +} diff --git a/pdns/dnsdistdist/dnsdist-nghttp2.hh b/pdns/dnsdistdist/dnsdist-nghttp2.hh index 5949cd3cdd..afc8737586 100644 --- a/pdns/dnsdistdist/dnsdist-nghttp2.hh +++ b/pdns/dnsdistdist/dnsdist-nghttp2.hh @@ -69,3 +69,5 @@ bool setupDoHClientProtocolNegotiation(std::shared_ptr& ctx); /* opens a new HTTP/2 connection to the supplied backend (attached to the supplied multiplexer), sends the query, waits for the response to come back or an error to occur then notifies the sender, closing the connection. */ bool sendH2Query(const std::shared_ptr& ds, std::unique_ptr& mplexer, std::shared_ptr& sender, InternalQuery&& query, bool healthCheck); +size_t handleH2Timeouts(FDMultiplexer& mplexer, const struct timeval& now); +size_t clearH2Connections(); diff --git a/pdns/dnsdistdist/dnsdist-tcp-downstream.cc b/pdns/dnsdistdist/dnsdist-tcp-downstream.cc index da37224087..5935b06448 100644 --- a/pdns/dnsdistdist/dnsdist-tcp-downstream.cc +++ b/pdns/dnsdistdist/dnsdist-tcp-downstream.cc @@ -190,9 +190,9 @@ void TCPConnectionToBackend::handleIO(std::shared_ptr& c if (connectionDied) { - DEBUGLOG("connection died, number of failures is "<d_downstreamFailures<<", retries is "<d_ds->retries); + DEBUGLOG("connection died, number of failures is "<d_downstreamFailures<<", retries is "<d_ds->d_retries); - if (conn->d_downstreamFailures < conn->d_ds->retries) { + if (conn->d_downstreamFailures < conn->d_ds->d_retries) { conn->d_ioState.reset(); ioGuard.release(); @@ -409,12 +409,12 @@ bool TCPConnectionToBackend::reconnect() catch (const std::runtime_error& e) { vinfolog("Connection to downstream server %s failed: %s", d_ds->getName(), e.what()); d_downstreamFailures++; - if (d_downstreamFailures >= d_ds->retries) { + if (d_downstreamFailures >= d_ds->d_retries) { throw; } } } - while (d_downstreamFailures < d_ds->retries); + while (d_downstreamFailures < d_ds->d_retries); return false; } diff --git a/pdns/dnsdistdist/dnsdist-tcp.hh b/pdns/dnsdistdist/dnsdist-tcp.hh index 312d0b72a4..2049cd3cdf 100644 --- a/pdns/dnsdistdist/dnsdist-tcp.hh +++ b/pdns/dnsdistdist/dnsdist-tcp.hh @@ -80,7 +80,7 @@ struct InternalQuery } InternalQuery(InternalQuery&& rhs) : - d_idstate(std::move(rhs.d_idstate)), d_buffer(std::move(rhs.d_buffer)), d_proxyProtocolPayload(std::move(rhs.d_proxyProtocolPayload)), d_xfrMasterSerial(rhs.d_xfrMasterSerial), d_xfrSerialCount(rhs.d_xfrSerialCount), d_xfrMasterSerialCount(rhs.d_xfrMasterSerialCount), d_proxyProtocolPayloadAdded(rhs.d_proxyProtocolPayloadAdded) + d_idstate(std::move(rhs.d_idstate)), d_buffer(std::move(rhs.d_buffer)), d_proxyProtocolPayload(std::move(rhs.d_proxyProtocolPayload)), d_xfrMasterSerial(rhs.d_xfrMasterSerial), d_xfrSerialCount(rhs.d_xfrSerialCount), d_downstreamFailures(rhs.d_downstreamFailures), d_xfrMasterSerialCount(rhs.d_xfrMasterSerialCount), d_proxyProtocolPayloadAdded(rhs.d_proxyProtocolPayloadAdded) { } InternalQuery& operator=(InternalQuery&& rhs) @@ -90,6 +90,7 @@ struct InternalQuery d_proxyProtocolPayload = std::move(rhs.d_proxyProtocolPayload); d_xfrMasterSerial = rhs.d_xfrMasterSerial; d_xfrSerialCount = rhs.d_xfrSerialCount; + d_downstreamFailures = rhs.d_downstreamFailures; d_xfrMasterSerialCount = rhs.d_xfrMasterSerialCount; d_proxyProtocolPayloadAdded = rhs.d_proxyProtocolPayloadAdded; return *this; @@ -108,6 +109,7 @@ struct InternalQuery std::string d_proxyProtocolPayload; uint32_t d_xfrMasterSerial{0}; uint32_t d_xfrSerialCount{0}; + uint32_t d_downstreamFailures{0}; uint8_t d_xfrMasterSerialCount{0}; bool d_xfrStarted{false}; bool d_proxyProtocolPayloadAdded{false}; diff --git a/pdns/dnsdistdist/test-dnsdistnghttp2_cc.cc b/pdns/dnsdistdist/test-dnsdistnghttp2_cc.cc new file mode 100644 index 0000000000..0723df5101 --- /dev/null +++ b/pdns/dnsdistdist/test-dnsdistnghttp2_cc.cc @@ -0,0 +1,1753 @@ +/* + * This file is part of PowerDNS or dnsdist. + * Copyright -- PowerDNS.COM B.V. and its contributors + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of version 2 of the GNU General Public License as + * published by the Free Software Foundation. + * + * In addition, for the avoidance of any doubt, permission is granted to + * link this program with OpenSSL and to (re)distribute the binaries + * produced as the result of such linking. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +#define BOOST_TEST_DYN_LINK +#define BOOST_TEST_NO_MAIN + +#include + +#include "dnswriter.hh" +#include "dnsdist.hh" +#include "dnsdist-proxy-protocol.hh" +#include "dnsdist-rings.hh" +#include "dnsdist-nghttp2.hh" + +#ifdef HAVE_NGHTTP2 +#include + +BOOST_AUTO_TEST_SUITE(test_dnsdistnghttp2_cc) + +struct ExpectedStep +{ +public: + enum class ExpectedRequest { handshakeClient, readFromClient, writeToClient, closeClient, connectToBackend, readFromBackend, writeToBackend, closeBackend }; + + ExpectedStep(ExpectedRequest r, IOState n, size_t b = 0, std::function fn = nullptr): cb(fn), request(r), nextState(n), bytes(b) + { + } + + std::function cb{nullptr}; + ExpectedRequest request; + IOState nextState; + size_t bytes{0}; +}; + +struct ExpectedData +{ + PacketBuffer d_query; + PacketBuffer d_response; +}; + +static std::deque s_steps; +static std::map s_responses; + +std::ostream& operator<<(std::ostream &os, const ExpectedStep::ExpectedRequest d); + +std::ostream& operator<<(std::ostream &os, const ExpectedStep::ExpectedRequest d) +{ + static const std::vector requests = { "handshake with client", "read from client", "write to client", "close connection to client", "connect to the backend", "read from the backend", "write to the backend", "close connection to backend" }; + os<(d)); + return os; +} + +struct DOHConnection +{ + DOHConnection(): d_session(std::unique_ptr(nullptr, nghttp2_session_del)) + { + nghttp2_session_callbacks* cbs = nullptr; + nghttp2_session_callbacks_new(&cbs); + std::unique_ptr callbacks(cbs, nghttp2_session_callbacks_del); + cbs = nullptr; + nghttp2_session_callbacks_set_send_callback(callbacks.get(), send_callback); + nghttp2_session_callbacks_set_on_frame_recv_callback(callbacks.get(), on_frame_recv_callback); + nghttp2_session_callbacks_set_on_data_chunk_recv_callback(callbacks.get(), on_data_chunk_recv_callback); + nghttp2_session_callbacks_set_on_stream_close_callback(callbacks.get(), on_stream_close_callback); + nghttp2_session* sess = nullptr; + nghttp2_session_server_new(&sess, callbacks.get(), this); + d_session = std::unique_ptr(sess, nghttp2_session_del); + + nghttp2_settings_entry iv[1] = { + {NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, 100}}; + nghttp2_submit_settings(d_session.get(), NGHTTP2_FLAG_NONE, iv, sizeof(iv)/sizeof(*iv)); + } + + PacketBuffer d_serverOutBuffer; + std::map d_queries; + std::map d_responses; + std::unique_ptr d_session; + /* used to replace the stream ID in outgoing frames. Ugly but the library does not let us + test weird cases without that */ + std::map d_idMapping; + + size_t submitIncoming(const PacketBuffer& data, size_t pos, size_t toWrite) + { + ssize_t readlen = nghttp2_session_mem_recv(d_session.get(), &data.at(pos), toWrite); + if (readlen < 0) { + throw("Fatal error while submitting: " + std::string(nghttp2_strerror(static_cast(readlen)))); + } + + /* just in case, see if we have anything to send */ + int rv = nghttp2_session_send(d_session.get()); + if (rv != 0) { + throw("Fatal error while sending: " + std::string(nghttp2_strerror(rv))); + } + + return readlen; + } + + void submitResponse(uint32_t streamId, PacketBuffer& data) + { + const nghttp2_nv hdrs[] = {(uint8_t*)":status", (uint8_t*)"200", sizeof(":status")-1, sizeof("200")-1, NGHTTP2_NV_FLAG_NONE}; + nghttp2_data_provider dataProvider; + dataProvider.source.ptr = &data; + dataProvider.read_callback = [](nghttp2_session* session, int32_t stream_id, uint8_t* buf, size_t length, uint32_t* data_flags, nghttp2_data_source* source, void* user_data) -> ssize_t { + auto buffer = reinterpret_cast(source->ptr); + size_t toCopy = 0; + if (buffer->size() > 0) { + toCopy = length > buffer->size() ? buffer->size() : length; + memcpy(buf, &buffer->at(0), toCopy); + buffer->erase(buffer->begin(), buffer->begin() + toCopy); + } + + if (buffer->size() == 0) { + *data_flags |= NGHTTP2_DATA_FLAG_EOF; + } + // cerr<<"submitting response data of size "<(user_data); + // cerr<<"inserting "<d_serverOutBuffer.size()<d_idMapping.empty() && length > 9) { + /* frame type == DATA */ + if (data[3] == NGHTTP2_DATA) { + uint32_t streamId = 0; + memcpy(&streamId, &data[5], sizeof(streamId)); + const auto it = conn->d_idMapping.find(ntohl(streamId)); + if (it != conn->d_idMapping.end()) { + streamId = htonl(it->second); + std::vector editedData(length); + std::copy(data, data + length, editedData.begin()); + memcpy(&editedData.at(5), &streamId, sizeof(streamId)); + conn->d_serverOutBuffer.insert(conn->d_serverOutBuffer.end(), editedData.data(), editedData.data() + length); + return static_cast(editedData.size()); + } + } + } + + conn->d_serverOutBuffer.insert(conn->d_serverOutBuffer.end(), data, data + length); + return static_cast(length); + } + + static int on_frame_recv_callback(nghttp2_session* session, const nghttp2_frame* frame, void* user_data) + { + DOHConnection* conn = reinterpret_cast(user_data); + // cerr<<"Frame type is "<hd.type)<hd.type == NGHTTP2_HEADERS || frame->hd.type == NGHTTP2_DATA) && frame->hd.flags & NGHTTP2_FLAG_END_STREAM) { +#if 0 + auto stream_data = nghttp2_session_get_stream_user_data(session, frame->hd.stream_id); + /* For DATA and HEADERS frame, this callback may be called after on_stream_close_callback. Check that stream still alive. */ + if (stream_data == nullptr) { + cerr<<"unable to find stream data!"<d_queries.at(frame->hd.stream_id); + BOOST_REQUIRE_GT(query.size(), sizeof(dnsheader)); + auto dh = reinterpret_cast(query.data()); + uint16_t id = ntohs(dh->id); + // cerr<<"got query ID "<(query.data()), query.size(), sizeof(dnsheader), false); + if (qname == DNSName("goaway.powerdns.com.")) { + conn->submitGoAway(); + } + else if (qname == DNSName("500.powerdns.com.") && (id % 2) == 0) { + /* we return a 500 on the first query only */ + conn->submitError(frame->hd.stream_id, 500, "Server failure"); + } + else if (qname == DNSName("wrong-stream-id.powerdns.com.") && (id % 2) == 0) { + /* we return a wrong stremad ID on the first query only */ + BOOST_CHECK_EQUAL(frame->hd.stream_id, 1U); + conn->d_responses[frame->hd.stream_id] = expected.d_response; + /* use an invalid stream ID! */ + conn->d_idMapping[frame->hd.stream_id] = frame->hd.stream_id + 4; + conn->submitResponse(frame->hd.stream_id, conn->d_responses.at(frame->hd.stream_id)); + } + else { + conn->d_responses[frame->hd.stream_id] = expected.d_response; + conn->submitResponse(frame->hd.stream_id, conn->d_responses.at(frame->hd.stream_id)); + } + conn->d_queries.erase(frame->hd.stream_id); + } + + return 0; + } + + static int on_data_chunk_recv_callback(nghttp2_session* session, uint8_t flags, int32_t stream_id, const uint8_t* data, size_t len, void* user_data) + { + DOHConnection* conn = reinterpret_cast(user_data); + // cerr<<"in "<<__PRETTY_FUNCTION__<d_queries[stream_id]; + query.insert(query.end(), data, data + len); + // cerr<<"out "<<__PRETTY_FUNCTION__<(user_data); + + if (error_code == 0) { + return 0; + } + + return 0; + } + +}; + +static std::map> s_connectionBuffers; + +class MockupTLSConnection : public TLSConnection +{ +public: + MockupTLSConnection(int descriptor, bool client = false): d_descriptor(descriptor), d_client(client) + { + s_connectionBuffers[d_descriptor] = std::make_unique(); + } + + ~MockupTLSConnection() { } + + IOState tryHandshake() override + { + auto step = getStep(); + BOOST_REQUIRE_EQUAL(step.request, ExpectedStep::ExpectedRequest::handshakeClient); + + return step.nextState; + } + + IOState tryWrite(const PacketBuffer& buffer, size_t& pos, size_t toWrite) override + { + auto& conn = s_connectionBuffers.at(d_descriptor); + auto step = getStep(); + BOOST_REQUIRE_EQUAL(step.request, !d_client ? ExpectedStep::ExpectedRequest::writeToClient : ExpectedStep::ExpectedRequest::writeToBackend); + + if (step.bytes == 0) { + if (step.nextState == IOState::NeedWrite) { + return step.nextState; + } + throw std::runtime_error("Remote host closed the connection"); + } + + toWrite -= pos; + BOOST_REQUIRE_GE(buffer.size(), pos + toWrite); + + if (step.bytes < toWrite) { + toWrite = step.bytes; + } + + conn->submitIncoming(buffer, pos, toWrite); + pos += toWrite; + + return step.nextState; + } + + IOState tryRead(PacketBuffer& buffer, size_t& pos, size_t toRead, bool allowIncomplete=false) override + { + auto& conn = s_connectionBuffers.at(d_descriptor); + auto step = getStep(); + BOOST_REQUIRE_EQUAL(step.request, !d_client ? ExpectedStep::ExpectedRequest::readFromClient : ExpectedStep::ExpectedRequest::readFromBackend); + + if (step.bytes == 0) { + if (step.nextState == IOState::NeedRead) { + return step.nextState; + } + throw std::runtime_error("Remote host closed the connection"); + } + + auto& externalBuffer = conn->d_serverOutBuffer; + toRead -= pos; + + if (step.bytes < toRead) { + toRead = step.bytes; + } + if (allowIncomplete) { + if (toRead > externalBuffer.size()) { + toRead = externalBuffer.size(); + } + } + else { + BOOST_REQUIRE_GE(externalBuffer.size(), toRead); + } + + BOOST_REQUIRE_GE(buffer.size(), toRead); + + // cerr<<"in server try read, adding "< getNextProtocol() const override + { + return std::vector(); + } + + LibsslTLSVersion getTLSVersion() const override + { + return LibsslTLSVersion::TLS13; + } + + bool hasSessionBeenResumed() const override + { + return false; + } + + std::vector> getSessions() override + { + return {}; + } + + void setSession(std::unique_ptr& session) override + { + } + + /* unused in that context, don't bother */ + void doHandshake() override + { + } + + void connect(bool fastOpen, const ComboAddress& remote, const struct timeval& timeout) override + { + } + + size_t read(void* buffer, size_t bufferSize, const struct timeval&readTimeout, const struct timeval& totalTimeout={0,0}, bool allowIncomplete=false) override + { + return 0; + } + + size_t write(const void* buffer, size_t bufferSize, const struct timeval& writeTimeout) override + { + return 0; + } +private: + ExpectedStep getStep() const + { + BOOST_REQUIRE(!s_steps.empty()); + auto step = s_steps.front(); + s_steps.pop_front(); + + if (step.cb) { + step.cb(d_descriptor, step); + } + + return step; + } + + const int d_descriptor; + bool d_client{false}; +}; + +class MockupTLSCtx : public TLSCtx +{ +public: + ~MockupTLSCtx() + { + } + + std::unique_ptr getConnection(int socket, const struct timeval& timeout, time_t now) override + { + return std::make_unique(socket); + } + + std::unique_ptr getClientConnection(const std::string& host, int socket, const struct timeval& timeout) override + { + return std::make_unique(socket, true); + } + + void rotateTicketsKey(time_t now) override + { + } + + size_t getTicketsKeysCount() override + { + return 0; + } + + std::string getName() const override + { + return "Mockup TLS"; + } +}; + +class MockupFDMultiplexer : public FDMultiplexer +{ +public: + MockupFDMultiplexer() + { + } + + ~MockupFDMultiplexer() + { + } + + int run(struct timeval* tv, int timeout=500) override + { + int ret = 0; + + gettimeofday(tv, nullptr); // MANDATORY + + /* 'ready' might be altered by a callback while we are iterating */ + const auto readyFDs = ready; + for (const auto fd : readyFDs) { + { + const auto& it = d_readCallbacks.find(fd); + + if (it != d_readCallbacks.end()) { + it->d_callback(it->d_fd, it->d_parameter); + } + } + + { + const auto& it = d_writeCallbacks.find(fd); + + if (it != d_writeCallbacks.end()) { + it->d_callback(it->d_fd, it->d_parameter); + } + } + } + + return ret; + } + + void getAvailableFDs(std::vector& fds, int timeout) override + { + } + + void addFD(int fd, FDMultiplexer::EventKind kind) override + { + } + + void removeFD(int fd, FDMultiplexer::EventKind) override + { + } + + string getName() const override + { + return "mockup"; + } + + void setReady(int fd) + { + ready.insert(fd); + } + + void setNotReady(int fd) + { + ready.erase(fd); + } + +private: + std::set ready; +}; + +class MockupQuerySender : public TCPQuerySender +{ +public: + bool active() const override + { + return true; + } + + const ClientState* getClientState() const override + { + return nullptr; + } + + void handleResponse(const struct timeval& now, TCPResponse&& response) override + { + if (d_customHandler) { + d_customHandler(d_id, now, std::move(response)); + return; + } + + BOOST_REQUIRE_GT(response.d_buffer.size(), sizeof(dnsheader)); + auto dh = reinterpret_cast(response.d_buffer.data()); + uint16_t id = ntohs(dh->id); + + BOOST_REQUIRE_EQUAL(id, d_id); + const auto& expected = s_responses.at(id); + BOOST_REQUIRE_EQUAL(expected.d_response.size(), response.d_buffer.size()); + for (size_t idx = 0; idx < response.d_buffer.size(); idx++) { + if (expected.d_response.at(idx) != response.d_buffer.at(idx)) { + cerr<<"Mismatch at offset "< d_customHandler; + uint16_t d_id{0}; + bool d_valid{false}; + bool d_error{false}; +}; + +static std::unique_ptr s_mplexer; + +struct TestFixture +{ + TestFixture() + { + s_steps.clear(); + s_responses.clear(); + s_mplexer = std::unique_ptr(new MockupFDMultiplexer()); + } + ~TestFixture() + { + clearH2Connections(); + s_steps.clear(); + s_responses.clear(); + s_mplexer.reset(); + } +}; + +BOOST_FIXTURE_TEST_CASE(test_SingleQuery, TestFixture) +{ + ComboAddress local("192.0.2.1:80"); + ClientState localCS(local, true, false, false, "", {}); + auto tlsCtx = std::make_shared(); + localCS.tlsFrontend = std::make_shared(tlsCtx); + + struct timeval now; + gettimeofday(&now, nullptr); + + size_t counter = 1; + DNSName name("powerdns.com."); + PacketBuffer query; + GenericDNSPacketWriter pwQ(query, name, QType::A, QClass::IN, 0); + pwQ.getHeader()->rd = 1; + pwQ.getHeader()->id = htons(counter); + + PacketBuffer response; + GenericDNSPacketWriter pwR(response, name, QType::A, QClass::IN, 0); + pwR.getHeader()->qr = 1; + pwR.getHeader()->rd = 1; + pwR.getHeader()->ra = 1; + pwR.getHeader()->id = htons(counter); + pwR.startRecord(name, QType::A, 7200, QClass::IN, DNSResourceRecord::ANSWER); + pwR.xfr32BitInt(0x01020304); + pwR.commit(); + + s_responses[counter] = {query, response}; + + auto backend = std::make_shared(ComboAddress("192.0.2.42:53"), ComboAddress("0.0.0.0:0"), 0, std::string(), 1, false); + backend->d_tlsCtx = tlsCtx; + backend->d_tlsSubjectName = "backend.powerdns.com"; + backend->d_dohPath = "/dns-query"; + backend->d_addXForwardedHeaders = true; + + auto sender = std::make_shared(); + sender->d_id = counter; + InternalQuery internalQuery(std::move(query), IDState()); + + s_steps = { + { ExpectedStep::ExpectedRequest::connectToBackend, IOState::Done }, + /* opening */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* settings */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* headers */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* data */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max(), [](int desc, const ExpectedStep& step) { + /* set the outgoing descriptor (backend connection) as ready */ + dynamic_cast(s_mplexer.get())->setReady(desc); + } }, + /* read settings, headers and response from the server */ + { ExpectedStep::ExpectedRequest::readFromBackend, IOState::Done, std::numeric_limits::max() }, + /* acknowledge settings */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + { ExpectedStep::ExpectedRequest::closeBackend, IOState::Done }, + }; + + auto sliced = std::shared_ptr(sender); + bool result = sendH2Query(backend, s_mplexer, sliced, std::move(internalQuery), false); + BOOST_CHECK_EQUAL(result, true); + + while (s_mplexer->getWatchedFDCount(false) != 0 || s_mplexer->getWatchedFDCount(true) != 0) { + s_mplexer->run(&now); + } + BOOST_CHECK_EQUAL(sender->d_valid, true); +} + +BOOST_FIXTURE_TEST_CASE(test_ConcurrentQueries, TestFixture) +{ + ComboAddress local("192.0.2.1:80"); + ClientState localCS(local, true, false, false, "", {}); + auto tlsCtx = std::make_shared(); + localCS.tlsFrontend = std::make_shared(tlsCtx); + + struct timeval now; + gettimeofday(&now, nullptr); + + auto backend = std::make_shared(ComboAddress("192.0.2.42:53"), ComboAddress("0.0.0.0:0"), 0, std::string(), 1, false); + backend->d_tlsCtx = tlsCtx; + backend->d_tlsSubjectName = "backend.powerdns.com"; + backend->d_dohPath = "/dns-query"; + backend->d_addXForwardedHeaders = true; + + size_t numberOfQueries = 2; + std::vector, InternalQuery>> queries; + for (size_t counter = 0; counter < numberOfQueries; counter++) { + DNSName name("powerdns.com."); + PacketBuffer query; + GenericDNSPacketWriter pwQ(query, name, QType::A, QClass::IN, 0); + pwQ.getHeader()->rd = 1; + pwQ.getHeader()->id = htons(counter); + + PacketBuffer response; + GenericDNSPacketWriter pwR(response, name, QType::A, QClass::IN, 0); + pwR.getHeader()->qr = 1; + pwR.getHeader()->rd = 1; + pwR.getHeader()->ra = 1; + pwR.getHeader()->id = htons(counter); + pwR.startRecord(name, QType::A, 7200, QClass::IN, DNSResourceRecord::ANSWER); + pwR.xfr32BitInt(0x01020304); + pwR.commit(); + + s_responses[counter] = {query, response}; + + auto sender = std::make_shared(); + sender->d_id = counter; + InternalQuery internalQuery(std::move(query), IDState()); + queries.push_back({std::move(sender), std::move(internalQuery)}); + } + + s_steps = { + { ExpectedStep::ExpectedRequest::connectToBackend, IOState::Done }, + /* opening */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* settings */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* headers */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* data */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max(), [](int desc, const ExpectedStep& step) { + /* set the outgoing descriptor (backend connection) as ready */ + dynamic_cast(s_mplexer.get())->setReady(desc); + } }, + /* headers */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* data */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max(), [](int desc, const ExpectedStep& step) { + /* set the outgoing descriptor (backend connection) as ready */ + dynamic_cast(s_mplexer.get())->setReady(desc); + } }, + /* read settings, headers and responses from the server */ + { ExpectedStep::ExpectedRequest::readFromBackend, IOState::Done, std::numeric_limits::max() }, + /* acknowledge settings */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + { ExpectedStep::ExpectedRequest::closeBackend, IOState::Done }, + }; + + for (auto& query : queries) { + auto sliced = std::static_pointer_cast(query.first); + bool result = sendH2Query(backend, s_mplexer, sliced, std::move(query.second), false); + BOOST_CHECK_EQUAL(result, true); + } + + while (s_mplexer->getWatchedFDCount(false) != 0 || s_mplexer->getWatchedFDCount(true) != 0) { + s_mplexer->run(&now); + } + + for (auto& query : queries) { + BOOST_CHECK_EQUAL(query.first->d_valid, true); + } +} + +BOOST_FIXTURE_TEST_CASE(test_ConnectionReuse, TestFixture) +{ + ComboAddress local("192.0.2.1:80"); + ClientState localCS(local, true, false, false, "", {}); + auto tlsCtx = std::make_shared(); + localCS.tlsFrontend = std::make_shared(tlsCtx); + + struct timeval now; + gettimeofday(&now, nullptr); + + auto backend = std::make_shared(ComboAddress("192.0.2.42:53"), ComboAddress("0.0.0.0:0"), 0, std::string(), 1, false); + backend->d_tlsCtx = tlsCtx; + backend->d_tlsSubjectName = "backend.powerdns.com"; + backend->d_dohPath = "/dns-query"; + backend->d_addXForwardedHeaders = true; + + size_t numberOfQueries = 2; + std::vector, InternalQuery>> queries; + for (size_t counter = 0; counter < numberOfQueries; counter++) { + DNSName name("powerdns.com."); + PacketBuffer query; + GenericDNSPacketWriter pwQ(query, name, QType::A, QClass::IN, 0); + pwQ.getHeader()->rd = 1; + pwQ.getHeader()->id = htons(counter); + + PacketBuffer response; + GenericDNSPacketWriter pwR(response, name, QType::A, QClass::IN, 0); + pwR.getHeader()->qr = 1; + pwR.getHeader()->rd = 1; + pwR.getHeader()->ra = 1; + pwR.getHeader()->id = htons(counter); + pwR.startRecord(name, QType::A, 7200, QClass::IN, DNSResourceRecord::ANSWER); + pwR.xfr32BitInt(0x01020304); + pwR.commit(); + + s_responses[counter] = {query, response}; + + auto sender = std::make_shared(); + sender->d_id = counter; + InternalQuery internalQuery(std::move(query), IDState()); + queries.push_back({std::move(sender), std::move(internalQuery)}); + } + + s_steps = { + { ExpectedStep::ExpectedRequest::connectToBackend, IOState::Done }, + /* opening */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* settings */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* headers */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* data */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max(), [](int desc, const ExpectedStep& step) { + /* set the outgoing descriptor (backend connection) as ready */ + dynamic_cast(s_mplexer.get())->setReady(desc); + } }, + /* read settings, headers and responses from the server */ + { ExpectedStep::ExpectedRequest::readFromBackend, IOState::Done, std::numeric_limits::max() }, + /* acknowledge settings */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* headers */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* data */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max(), [](int desc, const ExpectedStep& step) { + /* set the outgoing descriptor (backend connection) as ready */ + dynamic_cast(s_mplexer.get())->setReady(desc); + } }, + /* read settings, headers and responses from the server */ + { ExpectedStep::ExpectedRequest::readFromBackend, IOState::Done, std::numeric_limits::max() }, + { ExpectedStep::ExpectedRequest::closeBackend, IOState::Done }, + }; + + { + auto& query = queries.at(0); + auto sliced = std::static_pointer_cast(query.first); + bool result = sendH2Query(backend, s_mplexer, sliced, std::move(query.second), false); + BOOST_CHECK_EQUAL(result, true); + + while (s_mplexer->getWatchedFDCount(false) != 0 || s_mplexer->getWatchedFDCount(true) != 0) { + s_mplexer->run(&now); + } + + BOOST_CHECK_EQUAL(query.first->d_valid, true); + } + + { + auto& query = queries.at(1); + auto sliced = std::static_pointer_cast(query.first); + bool result = sendH2Query(backend, s_mplexer, sliced, std::move(query.second), false); + BOOST_CHECK_EQUAL(result, true); + + while (s_mplexer->getWatchedFDCount(false) != 0 || s_mplexer->getWatchedFDCount(true) != 0) { + s_mplexer->run(&now); + } + + BOOST_CHECK_EQUAL(query.first->d_valid, true); + } +} + +BOOST_FIXTURE_TEST_CASE(test_InvalidDNSAnswer, TestFixture) +{ + ComboAddress local("192.0.2.1:80"); + ClientState localCS(local, true, false, false, "", {}); + auto tlsCtx = std::make_shared(); + localCS.tlsFrontend = std::make_shared(tlsCtx); + + struct timeval now; + gettimeofday(&now, nullptr); + + size_t counter = 1; + DNSName name("powerdns.com."); + PacketBuffer query; + GenericDNSPacketWriter pwQ(query, name, QType::A, QClass::IN, 0); + pwQ.getHeader()->rd = 1; + pwQ.getHeader()->id = htons(counter); + + PacketBuffer response; + GenericDNSPacketWriter pwR(response, name, QType::A, QClass::IN, 0); + pwR.getHeader()->qr = 1; + pwR.getHeader()->rd = 1; + pwR.getHeader()->ra = 1; + pwR.getHeader()->id = htons(counter); + pwR.startRecord(name, QType::A, 7200, QClass::IN, DNSResourceRecord::ANSWER); + pwR.xfr32BitInt(0x01020304); + pwR.commit(); + + /* TRUNCATE the answer */ + response.resize(11); + s_responses[counter] = {query, response}; + + auto backend = std::make_shared(ComboAddress("192.0.2.42:53"), ComboAddress("0.0.0.0:0"), 0, std::string(), 1, false); + backend->d_tlsCtx = tlsCtx; + backend->d_tlsSubjectName = "backend.powerdns.com"; + backend->d_dohPath = "/dns-query"; + backend->d_addXForwardedHeaders = true; + + auto sender = std::make_shared(); + sender->d_id = counter; + sender->d_customHandler = [](uint16_t id, const struct timeval&, TCPResponse&& resp) { + BOOST_CHECK_EQUAL(resp.d_buffer.size(), 11U); + /* simulate an exception, since DoH and UDP frontends will process the query right away, + while TCP and DoT will first pass it back to the TCP worker thread */ + throw std::runtime_error("Invalid response"); + }; + InternalQuery internalQuery(std::move(query), IDState()); + + s_steps = { + { ExpectedStep::ExpectedRequest::connectToBackend, IOState::Done }, + /* opening */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* settings */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* headers */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* data */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max(), [](int desc, const ExpectedStep& step) { + /* set the outgoing descriptor (backend connection) as ready */ + dynamic_cast(s_mplexer.get())->setReady(desc); + } }, + /* read settings, headers and response from the server */ + { ExpectedStep::ExpectedRequest::readFromBackend, IOState::Done, std::numeric_limits::max() }, + /* acknowledge settings */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + { ExpectedStep::ExpectedRequest::closeBackend, IOState::Done }, + }; + + auto sliced = std::shared_ptr(sender); + bool result = sendH2Query(backend, s_mplexer, sliced, std::move(internalQuery), false); + BOOST_CHECK_EQUAL(result, true); + + while (s_mplexer->getWatchedFDCount(false) != 0 || s_mplexer->getWatchedFDCount(true) != 0) { + s_mplexer->run(&now); + } + BOOST_CHECK_EQUAL(sender->d_valid, false); +} + +BOOST_FIXTURE_TEST_CASE(test_TimeoutWhileWriting, TestFixture) +{ + ComboAddress local("192.0.2.1:80"); + ClientState localCS(local, true, false, false, "", {}); + auto tlsCtx = std::make_shared(); + localCS.tlsFrontend = std::make_shared(tlsCtx); + + struct timeval now; + gettimeofday(&now, nullptr); + + auto backend = std::make_shared(ComboAddress("192.0.2.42:53"), ComboAddress("0.0.0.0:0"), 0, std::string(), 1, false); + backend->d_tlsCtx = tlsCtx; + backend->d_tlsSubjectName = "backend.powerdns.com"; + backend->d_dohPath = "/dns-query"; + backend->d_addXForwardedHeaders = true; + + size_t numberOfQueries = 2; + std::vector, InternalQuery>> queries; + for (size_t counter = 0; counter < numberOfQueries; counter++) { + DNSName name("powerdns.com."); + PacketBuffer query; + GenericDNSPacketWriter pwQ(query, name, QType::A, QClass::IN, 0); + pwQ.getHeader()->rd = 1; + pwQ.getHeader()->id = htons(counter); + + PacketBuffer response; + GenericDNSPacketWriter pwR(response, name, QType::A, QClass::IN, 0); + pwR.getHeader()->qr = 1; + pwR.getHeader()->rd = 1; + pwR.getHeader()->ra = 1; + pwR.getHeader()->id = htons(counter); + pwR.startRecord(name, QType::A, 7200, QClass::IN, DNSResourceRecord::ANSWER); + pwR.xfr32BitInt(0x01020304); + pwR.commit(); + + s_responses[counter] = {query, response}; + + auto sender = std::make_shared(); + sender->d_id = counter; + InternalQuery internalQuery(std::move(query), IDState()); + queries.push_back({std::move(sender), std::move(internalQuery)}); + } + + bool timeout = false; + s_steps = { + { ExpectedStep::ExpectedRequest::connectToBackend, IOState::Done }, + /* opening */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* settings */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* headers */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* data */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* headers */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* data */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::NeedWrite, std::numeric_limits::max(), [&timeout](int desc, const ExpectedStep& step) { + timeout = true; + } }, + { ExpectedStep::ExpectedRequest::closeBackend, IOState::Done }, + }; + + for (auto& query : queries) { + auto sliced = std::static_pointer_cast(query.first); + bool result = sendH2Query(backend, s_mplexer, sliced, std::move(query.second), false); + BOOST_CHECK_EQUAL(result, true); + } + + while (!timeout && (s_mplexer->getWatchedFDCount(false) != 0 || s_mplexer->getWatchedFDCount(true) != 0)) { + s_mplexer->run(&now); + } + + struct timeval later = now; + later.tv_sec += backend->tcpSendTimeout + 1; + + auto expiredConns = handleH2Timeouts(*s_mplexer, later); + BOOST_CHECK_EQUAL(expiredConns, 1U); + + for (auto& query : queries) { + BOOST_CHECK_EQUAL(query.first->d_valid, false); + BOOST_CHECK_EQUAL(query.first->d_error, true); + } + + BOOST_CHECK_EQUAL(clearH2Connections(), 0U); +} + +BOOST_FIXTURE_TEST_CASE(test_TimeoutWhileReading, TestFixture) +{ + ComboAddress local("192.0.2.1:80"); + ClientState localCS(local, true, false, false, "", {}); + auto tlsCtx = std::make_shared(); + localCS.tlsFrontend = std::make_shared(tlsCtx); + + struct timeval now; + gettimeofday(&now, nullptr); + + auto backend = std::make_shared(ComboAddress("192.0.2.42:53"), ComboAddress("0.0.0.0:0"), 0, std::string(), 1, false); + backend->d_tlsCtx = tlsCtx; + backend->d_tlsSubjectName = "backend.powerdns.com"; + backend->d_dohPath = "/dns-query"; + backend->d_addXForwardedHeaders = true; + + size_t numberOfQueries = 2; + std::vector, InternalQuery>> queries; + for (size_t counter = 0; counter < numberOfQueries; counter++) { + DNSName name("powerdns.com."); + PacketBuffer query; + GenericDNSPacketWriter pwQ(query, name, QType::A, QClass::IN, 0); + pwQ.getHeader()->rd = 1; + pwQ.getHeader()->id = htons(counter); + + PacketBuffer response; + GenericDNSPacketWriter pwR(response, name, QType::A, QClass::IN, 0); + pwR.getHeader()->qr = 1; + pwR.getHeader()->rd = 1; + pwR.getHeader()->ra = 1; + pwR.getHeader()->id = htons(counter); + pwR.startRecord(name, QType::A, 7200, QClass::IN, DNSResourceRecord::ANSWER); + pwR.xfr32BitInt(0x01020304); + pwR.commit(); + + s_responses[counter] = {query, response}; + + auto sender = std::make_shared(); + sender->d_id = counter; + InternalQuery internalQuery(std::move(query), IDState()); + queries.push_back({std::move(sender), std::move(internalQuery)}); + } + + bool timeout = false; + s_steps = { + { ExpectedStep::ExpectedRequest::connectToBackend, IOState::Done }, + /* opening */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* settings */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* headers */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* data */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* headers */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* data */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max(), [&timeout](int desc, const ExpectedStep& step) { + /* set the timeout flag now, since the timeout occurs while waiting for the descriptor to become readable */ + timeout = true; + } }, + { ExpectedStep::ExpectedRequest::closeBackend, IOState::Done }, +}; + + for (auto& query : queries) { + auto sliced = std::static_pointer_cast(query.first); + bool result = sendH2Query(backend, s_mplexer, sliced, std::move(query.second), false); + BOOST_CHECK_EQUAL(result, true); + } + + while (!timeout && (s_mplexer->getWatchedFDCount(false) != 0 || s_mplexer->getWatchedFDCount(true) != 0)) { + s_mplexer->run(&now); + } + + struct timeval later = now; + later.tv_sec += backend->tcpRecvTimeout + 1; + + auto expiredConns = handleH2Timeouts(*s_mplexer, later); + BOOST_CHECK_EQUAL(expiredConns, 1U); + + for (auto& query : queries) { + BOOST_CHECK_EQUAL(query.first->d_valid, false); + BOOST_CHECK_EQUAL(query.first->d_error, true); + } + BOOST_CHECK_EQUAL(clearH2Connections(), 0U); +} + +BOOST_FIXTURE_TEST_CASE(test_ShortWrite, TestFixture) +{ + ComboAddress local("192.0.2.1:80"); + ClientState localCS(local, true, false, false, "", {}); + auto tlsCtx = std::make_shared(); + localCS.tlsFrontend = std::make_shared(tlsCtx); + + struct timeval now; + gettimeofday(&now, nullptr); + + auto backend = std::make_shared(ComboAddress("192.0.2.42:53"), ComboAddress("0.0.0.0:0"), 0, std::string(), 1, false); + backend->d_tlsCtx = tlsCtx; + backend->d_tlsSubjectName = "backend.powerdns.com"; + backend->d_dohPath = "/dns-query"; + backend->d_addXForwardedHeaders = true; + + size_t numberOfQueries = 2; + std::vector, InternalQuery>> queries; + for (size_t counter = 0; counter < numberOfQueries; counter++) { + DNSName name("powerdns.com."); + PacketBuffer query; + GenericDNSPacketWriter pwQ(query, name, QType::A, QClass::IN, 0); + pwQ.getHeader()->rd = 1; + pwQ.getHeader()->id = htons(counter); + + PacketBuffer response; + GenericDNSPacketWriter pwR(response, name, QType::A, QClass::IN, 0); + pwR.getHeader()->qr = 1; + pwR.getHeader()->rd = 1; + pwR.getHeader()->ra = 1; + pwR.getHeader()->id = htons(counter); + pwR.startRecord(name, QType::A, 7200, QClass::IN, DNSResourceRecord::ANSWER); + pwR.xfr32BitInt(0x01020304); + pwR.commit(); + + s_responses[counter] = {query, response}; + + auto sender = std::make_shared(); + sender->d_id = counter; + InternalQuery internalQuery(std::move(query), IDState()); + queries.push_back({std::move(sender), std::move(internalQuery)}); + } + + s_steps = { + { ExpectedStep::ExpectedRequest::connectToBackend, IOState::Done }, + /* opening */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* settings */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::NeedWrite, 2, [](int desc, const ExpectedStep& step) { + /* set the outgoing descriptor (backend connection) as ready */ + dynamic_cast(s_mplexer.get())->setReady(desc); + } }, + /* settings (second attempt) + headers + data + headers (second query) + data */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max(), }, + /* read settings, headers and responses from the server */ + { ExpectedStep::ExpectedRequest::readFromBackend, IOState::Done, std::numeric_limits::max() }, + /* acknowledge settings */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + { ExpectedStep::ExpectedRequest::closeBackend, IOState::Done }, + }; + + for (auto& query : queries) { + auto sliced = std::static_pointer_cast(query.first); + bool result = sendH2Query(backend, s_mplexer, sliced, std::move(query.second), false); + BOOST_CHECK_EQUAL(result, true); + } + + while (s_mplexer->getWatchedFDCount(false) != 0 || s_mplexer->getWatchedFDCount(true) != 0) { + s_mplexer->run(&now); + } + + for (auto& query : queries) { + BOOST_CHECK_EQUAL(query.first->d_valid, true); + } + + BOOST_CHECK_EQUAL(clearH2Connections(), 1U); +} + +BOOST_FIXTURE_TEST_CASE(test_ShortRead, TestFixture) +{ + ComboAddress local("192.0.2.1:80"); + ClientState localCS(local, true, false, false, "", {}); + auto tlsCtx = std::make_shared(); + localCS.tlsFrontend = std::make_shared(tlsCtx); + + struct timeval now; + gettimeofday(&now, nullptr); + + auto backend = std::make_shared(ComboAddress("192.0.2.42:53"), ComboAddress("0.0.0.0:0"), 0, std::string(), 1, false); + backend->d_tlsCtx = tlsCtx; + backend->d_tlsSubjectName = "backend.powerdns.com"; + backend->d_dohPath = "/dns-query"; + backend->d_addXForwardedHeaders = true; + + size_t numberOfQueries = 2; + std::vector, InternalQuery>> queries; + for (size_t counter = 0; counter < numberOfQueries; counter++) { + DNSName name("powerdns.com."); + PacketBuffer query; + GenericDNSPacketWriter pwQ(query, name, QType::A, QClass::IN, 0); + pwQ.getHeader()->rd = 1; + pwQ.getHeader()->id = htons(counter); + + PacketBuffer response; + GenericDNSPacketWriter pwR(response, name, QType::A, QClass::IN, 0); + pwR.getHeader()->qr = 1; + pwR.getHeader()->rd = 1; + pwR.getHeader()->ra = 1; + pwR.getHeader()->id = htons(counter); + pwR.startRecord(name, QType::A, 7200, QClass::IN, DNSResourceRecord::ANSWER); + pwR.xfr32BitInt(0x01020304); + pwR.commit(); + + s_responses[counter] = {query, response}; + + auto sender = std::make_shared(); + sender->d_id = counter; + InternalQuery internalQuery(std::move(query), IDState()); + queries.push_back({std::move(sender), std::move(internalQuery)}); + } + + s_steps = { + { ExpectedStep::ExpectedRequest::connectToBackend, IOState::Done }, + /* opening */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* settings */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* headers */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* data */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max(), [](int desc, const ExpectedStep& step) { + /* set the outgoing descriptor (backend connection) as ready */ + dynamic_cast(s_mplexer.get())->setReady(desc); + } }, + /* headers */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* data */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max(), [](int desc, const ExpectedStep& step) { + /* set the outgoing descriptor (backend connection) as ready */ + dynamic_cast(s_mplexer.get())->setReady(desc); + } }, + /* read settings, headers and responses from the server */ + { ExpectedStep::ExpectedRequest::readFromBackend, IOState::NeedRead, 4 }, + /* read settings, headers and responses (second attempt) */ + { ExpectedStep::ExpectedRequest::readFromBackend, IOState::Done, std::numeric_limits::max() }, + /* acknowledge settings */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + { ExpectedStep::ExpectedRequest::closeBackend, IOState::Done }, + }; + + for (auto& query : queries) { + auto sliced = std::static_pointer_cast(query.first); + bool result = sendH2Query(backend, s_mplexer, sliced, std::move(query.second), false); + BOOST_CHECK_EQUAL(result, true); + } + + while (s_mplexer->getWatchedFDCount(false) != 0 || s_mplexer->getWatchedFDCount(true) != 0) { + s_mplexer->run(&now); + } + + for (auto& query : queries) { + BOOST_CHECK_EQUAL(query.first->d_valid, true); + } + + BOOST_CHECK_EQUAL(clearH2Connections(), 1U); +} + +BOOST_FIXTURE_TEST_CASE(test_ConnectionClosedWhileReading, TestFixture) +{ + ComboAddress local("192.0.2.1:80"); + ClientState localCS(local, true, false, false, "", {}); + auto tlsCtx = std::make_shared(); + localCS.tlsFrontend = std::make_shared(tlsCtx); + + struct timeval now; + gettimeofday(&now, nullptr); + + auto backend = std::make_shared(ComboAddress("192.0.2.42:53"), ComboAddress("0.0.0.0:0"), 0, std::string(), 1, false); + backend->d_tlsCtx = tlsCtx; + backend->d_tlsSubjectName = "backend.powerdns.com"; + backend->d_dohPath = "/dns-query"; + backend->d_addXForwardedHeaders = true; + + size_t numberOfQueries = 2; + std::vector, InternalQuery>> queries; + for (size_t counter = 0; counter < numberOfQueries; counter++) { + DNSName name("powerdns.com."); + PacketBuffer query; + GenericDNSPacketWriter pwQ(query, name, QType::A, QClass::IN, 0); + pwQ.getHeader()->rd = 1; + pwQ.getHeader()->id = htons(counter); + + PacketBuffer response; + GenericDNSPacketWriter pwR(response, name, QType::A, QClass::IN, 0); + pwR.getHeader()->qr = 1; + pwR.getHeader()->rd = 1; + pwR.getHeader()->ra = 1; + pwR.getHeader()->id = htons(counter); + pwR.startRecord(name, QType::A, 7200, QClass::IN, DNSResourceRecord::ANSWER); + pwR.xfr32BitInt(0x01020304); + pwR.commit(); + + s_responses[counter] = {query, response}; + + auto sender = std::make_shared(); + sender->d_id = counter; + InternalQuery internalQuery(std::move(query), IDState()); + queries.push_back({std::move(sender), std::move(internalQuery)}); + } + + s_steps = { + { ExpectedStep::ExpectedRequest::connectToBackend, IOState::Done }, + /* opening */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* settings */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* headers */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* data */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max(), [](int desc, const ExpectedStep& step) { + /* set the outgoing descriptor (backend connection) as ready */ + dynamic_cast(s_mplexer.get())->setReady(desc); + } }, + /* headers */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* data */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max(), [](int desc, const ExpectedStep& step) { + /* set the outgoing descriptor (backend connection) as ready */ + dynamic_cast(s_mplexer.get())->setReady(desc); + } }, + /* read settings, headers and responses from the server */ + { ExpectedStep::ExpectedRequest::readFromBackend, IOState::Done, 0 }, + { ExpectedStep::ExpectedRequest::closeBackend, IOState::Done }, + }; + + for (auto& query : queries) { + auto sliced = std::static_pointer_cast(query.first); + bool result = sendH2Query(backend, s_mplexer, sliced, std::move(query.second), false); + BOOST_CHECK_EQUAL(result, true); + } + + while (s_mplexer->getWatchedFDCount(false) != 0 || s_mplexer->getWatchedFDCount(true) != 0) { + s_mplexer->run(&now); + } + + for (auto& query : queries) { + BOOST_CHECK_EQUAL(query.first->d_valid, false); + BOOST_CHECK_EQUAL(query.first->d_error, true); + } + + BOOST_CHECK_EQUAL(clearH2Connections(), 0U); +} + +BOOST_FIXTURE_TEST_CASE(test_ConnectionClosedWhileWriting, TestFixture) +{ + ComboAddress local("192.0.2.1:80"); + ClientState localCS(local, true, false, false, "", {}); + auto tlsCtx = std::make_shared(); + localCS.tlsFrontend = std::make_shared(tlsCtx); + + struct timeval now; + gettimeofday(&now, nullptr); + + auto backend = std::make_shared(ComboAddress("192.0.2.42:53"), ComboAddress("0.0.0.0:0"), 0, std::string(), 1, false); + backend->d_tlsCtx = tlsCtx; + backend->d_tlsSubjectName = "backend.powerdns.com"; + backend->d_dohPath = "/dns-query"; + backend->d_addXForwardedHeaders = true; + + size_t numberOfQueries = 2; + std::vector, InternalQuery>> queries; + for (size_t counter = 0; counter < numberOfQueries; counter++) { + DNSName name("powerdns.com."); + PacketBuffer query; + GenericDNSPacketWriter pwQ(query, name, QType::A, QClass::IN, 0); + pwQ.getHeader()->rd = 1; + pwQ.getHeader()->id = htons(counter); + + PacketBuffer response; + GenericDNSPacketWriter pwR(response, name, QType::A, QClass::IN, 0); + pwR.getHeader()->qr = 1; + pwR.getHeader()->rd = 1; + pwR.getHeader()->ra = 1; + pwR.getHeader()->id = htons(counter); + pwR.startRecord(name, QType::A, 7200, QClass::IN, DNSResourceRecord::ANSWER); + pwR.xfr32BitInt(0x01020304); + pwR.commit(); + + s_responses[counter] = {query, response}; + + auto sender = std::make_shared(); + sender->d_id = counter; + InternalQuery internalQuery(std::move(query), IDState()); + queries.push_back({std::move(sender), std::move(internalQuery)}); + } + + s_steps = { + { ExpectedStep::ExpectedRequest::connectToBackend, IOState::Done }, + /* opening */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* settings */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* headers, connection is closed by the backend */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, 0 }, + { ExpectedStep::ExpectedRequest::closeBackend, IOState::Done }, + { ExpectedStep::ExpectedRequest::connectToBackend, IOState::Done }, + /* opening */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* settings */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* headers */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* data */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max(), [](int desc, const ExpectedStep& step) { + /* set the outgoing descriptor (backend connection) as ready */ + dynamic_cast(s_mplexer.get())->setReady(desc); + } }, + /* read settings, headers and response from the server */ + { ExpectedStep::ExpectedRequest::readFromBackend, IOState::Done, std::numeric_limits::max() }, + /* acknowledge settings */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + { ExpectedStep::ExpectedRequest::closeBackend, IOState::Done }, + }; + + for (auto& query : queries) { + auto sliced = std::static_pointer_cast(query.first); + bool result = sendH2Query(backend, s_mplexer, sliced, std::move(query.second), false); + BOOST_CHECK_EQUAL(result, true); + } + + while (s_mplexer->getWatchedFDCount(false) != 0 || s_mplexer->getWatchedFDCount(true) != 0) { + s_mplexer->run(&now); + } + + BOOST_CHECK_EQUAL(queries.at(0).first->d_valid, false); + BOOST_CHECK_EQUAL(queries.at(0).first->d_error, true); + BOOST_CHECK_EQUAL(queries.at(1).first->d_valid, true); + BOOST_CHECK_EQUAL(queries.at(1).first->d_error, false); + + BOOST_CHECK_EQUAL(clearH2Connections(), 1U); +} + +BOOST_FIXTURE_TEST_CASE(test_GoAwayFromServer, TestFixture) +{ + ComboAddress local("192.0.2.1:80"); + ClientState localCS(local, true, false, false, "", {}); + auto tlsCtx = std::make_shared(); + localCS.tlsFrontend = std::make_shared(tlsCtx); + + struct timeval now; + gettimeofday(&now, nullptr); + + auto backend = std::make_shared(ComboAddress("192.0.2.42:53"), ComboAddress("0.0.0.0:0"), 0, std::string(), 1, false); + backend->d_tlsCtx = tlsCtx; + backend->d_tlsSubjectName = "backend.powerdns.com"; + backend->d_dohPath = "/dns-query"; + backend->d_addXForwardedHeaders = true; + /* set the number of reconnection attempts to a low value to not waste time */ + backend->d_retries = 1; + + size_t numberOfQueries = 2; + std::vector, InternalQuery>> queries; + for (size_t counter = 0; counter < numberOfQueries; counter++) { + DNSName name("goaway.powerdns.com."); + PacketBuffer query; + GenericDNSPacketWriter pwQ(query, name, QType::A, QClass::IN, 0); + pwQ.getHeader()->rd = 1; + pwQ.getHeader()->id = htons(counter); + + PacketBuffer response; + GenericDNSPacketWriter pwR(response, name, QType::A, QClass::IN, 0); + pwR.getHeader()->qr = 1; + pwR.getHeader()->rd = 1; + pwR.getHeader()->ra = 1; + pwR.getHeader()->id = htons(counter); + pwR.startRecord(name, QType::A, 7200, QClass::IN, DNSResourceRecord::ANSWER); + pwR.xfr32BitInt(0x01020304); + pwR.commit(); + + s_responses[counter] = {query, response}; + + auto sender = std::make_shared(); + sender->d_id = counter; + InternalQuery internalQuery(std::move(query), IDState()); + queries.push_back({std::move(sender), std::move(internalQuery)}); + } + + s_steps = { + { ExpectedStep::ExpectedRequest::connectToBackend, IOState::Done }, + /* opening */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* settings */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* headers */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* data */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max(), [](int desc, const ExpectedStep& step) { + /* set the outgoing descriptor (backend connection) as ready */ + dynamic_cast(s_mplexer.get())->setReady(desc); + } }, + /* headers */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* data */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max(), [](int desc, const ExpectedStep& step) { + /* set the outgoing descriptor (backend connection) as ready */ + dynamic_cast(s_mplexer.get())->setReady(desc); + } }, + /* read GO AWAY from the server (1) */ + { ExpectedStep::ExpectedRequest::readFromBackend, IOState::Done, std::numeric_limits::max() }, + { ExpectedStep::ExpectedRequest::connectToBackend, IOState::Done }, + /* opening */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* settings */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* headers */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* data */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max(), [](int desc, const ExpectedStep& step) { + /* set the outgoing descriptor (backend connection) as ready */ + dynamic_cast(s_mplexer.get())->setReady(desc); + } }, + /* headers */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* data */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max(), [](int desc, const ExpectedStep& step) { + /* set the outgoing descriptor (backend connection) as ready */ + dynamic_cast(s_mplexer.get())->setReady(desc); + } }, + /* close the first connection. It happens now because the new connection was set up first, then that one destroyed */ + { ExpectedStep::ExpectedRequest::closeBackend, IOState::Done }, + /* read GO AWAY from the server (1) */ + { ExpectedStep::ExpectedRequest::readFromBackend, IOState::Done, std::numeric_limits::max() }, + { ExpectedStep::ExpectedRequest::closeBackend, IOState::Done }, + }; + + for (auto& query : queries) { + auto sliced = std::static_pointer_cast(query.first); + bool result = sendH2Query(backend, s_mplexer, sliced, std::move(query.second), false); + BOOST_CHECK_EQUAL(result, true); + } + + while (s_mplexer->getWatchedFDCount(false) != 0 || s_mplexer->getWatchedFDCount(true) != 0) { + s_mplexer->run(&now); + } + + for (auto& query : queries) { + BOOST_CHECK_EQUAL(query.first->d_valid, false); + BOOST_CHECK_EQUAL(query.first->d_error, true); + } + + BOOST_CHECK_EQUAL(clearH2Connections(), 0U); +} + +BOOST_FIXTURE_TEST_CASE(test_HTTP500FromServer, TestFixture) +{ + ComboAddress local("192.0.2.1:80"); + ClientState localCS(local, true, false, false, "", {}); + auto tlsCtx = std::make_shared(); + localCS.tlsFrontend = std::make_shared(tlsCtx); + + struct timeval now; + gettimeofday(&now, nullptr); + + auto backend = std::make_shared(ComboAddress("192.0.2.42:53"), ComboAddress("0.0.0.0:0"), 0, std::string(), 1, false); + backend->d_tlsCtx = tlsCtx; + backend->d_tlsSubjectName = "backend.powerdns.com"; + backend->d_dohPath = "/dns-query"; + backend->d_addXForwardedHeaders = true; + + size_t numberOfQueries = 2; + std::vector, InternalQuery>> queries; + for (size_t counter = 0; counter < numberOfQueries; counter++) { + DNSName name("500.powerdns.com."); + PacketBuffer query; + GenericDNSPacketWriter pwQ(query, name, QType::A, QClass::IN, 0); + pwQ.getHeader()->rd = 1; + pwQ.getHeader()->id = htons(counter); + + PacketBuffer response; + GenericDNSPacketWriter pwR(response, name, QType::A, QClass::IN, 0); + pwR.getHeader()->qr = 1; + pwR.getHeader()->rd = 1; + pwR.getHeader()->ra = 1; + pwR.getHeader()->id = htons(counter); + pwR.startRecord(name, QType::A, 7200, QClass::IN, DNSResourceRecord::ANSWER); + pwR.xfr32BitInt(0x01020304); + pwR.commit(); + + s_responses[counter] = {query, response}; + + auto sender = std::make_shared(); + sender->d_id = counter; + InternalQuery internalQuery(std::move(query), IDState()); + queries.push_back({std::move(sender), std::move(internalQuery)}); + } + + s_steps = { + { ExpectedStep::ExpectedRequest::connectToBackend, IOState::Done }, + /* opening */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* settings */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* headers */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* data */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max(), [](int desc, const ExpectedStep& step) { + /* set the outgoing descriptor (backend connection) as ready */ + dynamic_cast(s_mplexer.get())->setReady(desc); + } }, + /* headers */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* data */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max(), [](int desc, const ExpectedStep& step) { + /* set the outgoing descriptor (backend connection) as ready */ + dynamic_cast(s_mplexer.get())->setReady(desc); + } }, + /* read settings, headers and responses from the server */ + { ExpectedStep::ExpectedRequest::readFromBackend, IOState::Done, std::numeric_limits::max() }, + /* acknowledge settings */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + { ExpectedStep::ExpectedRequest::closeBackend, IOState::Done }, + }; + + for (auto& query : queries) { + auto sliced = std::static_pointer_cast(query.first); + bool result = sendH2Query(backend, s_mplexer, sliced, std::move(query.second), false); + BOOST_CHECK_EQUAL(result, true); + } + + while (s_mplexer->getWatchedFDCount(false) != 0 || s_mplexer->getWatchedFDCount(true) != 0) { + s_mplexer->run(&now); + } + + BOOST_CHECK_EQUAL(queries.at(0).first->d_valid, false); + BOOST_CHECK_EQUAL(queries.at(0).first->d_error, true); + BOOST_CHECK_EQUAL(queries.at(1).first->d_valid, true); + BOOST_CHECK_EQUAL(queries.at(1).first->d_error, false); + + BOOST_CHECK_EQUAL(clearH2Connections(), 1U); +} + +BOOST_FIXTURE_TEST_CASE(test_WrongStreamID, TestFixture) +{ + ComboAddress local("192.0.2.1:80"); + ClientState localCS(local, true, false, false, "", {}); + auto tlsCtx = std::make_shared(); + localCS.tlsFrontend = std::make_shared(tlsCtx); + + struct timeval now; + gettimeofday(&now, nullptr); + + auto backend = std::make_shared(ComboAddress("192.0.2.42:53"), ComboAddress("0.0.0.0:0"), 0, std::string(), 1, false); + backend->d_tlsCtx = tlsCtx; + backend->d_tlsSubjectName = "backend.powerdns.com"; + backend->d_dohPath = "/dns-query"; + backend->d_addXForwardedHeaders = true; + + size_t numberOfQueries = 2; + std::vector, InternalQuery>> queries; + for (size_t counter = 0; counter < numberOfQueries; counter++) { + DNSName name("wrong-stream-id.powerdns.com."); + PacketBuffer query; + GenericDNSPacketWriter pwQ(query, name, QType::A, QClass::IN, 0); + pwQ.getHeader()->rd = 1; + pwQ.getHeader()->id = htons(counter); + + PacketBuffer response; + GenericDNSPacketWriter pwR(response, name, QType::A, QClass::IN, 0); + pwR.getHeader()->qr = 1; + pwR.getHeader()->rd = 1; + pwR.getHeader()->ra = 1; + pwR.getHeader()->id = htons(counter); + pwR.startRecord(name, QType::A, 7200, QClass::IN, DNSResourceRecord::ANSWER); + pwR.xfr32BitInt(0x01020304); + pwR.commit(); + + s_responses[counter] = {query, response}; + + auto sender = std::make_shared(); + sender->d_id = counter; + InternalQuery internalQuery(std::move(query), IDState()); + queries.push_back({std::move(sender), std::move(internalQuery)}); + } + + bool timeout = false; + s_steps = { + { ExpectedStep::ExpectedRequest::connectToBackend, IOState::Done }, + /* opening */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* settings */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* headers */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* data */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max(), [](int desc, const ExpectedStep& step) { + /* set the outgoing descriptor (backend connection) as ready */ + dynamic_cast(s_mplexer.get())->setReady(desc); + } }, + /* headers */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* data */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max(), [](int desc, const ExpectedStep& step) { + /* set the outgoing descriptor (backend connection) as ready */ + dynamic_cast(s_mplexer.get())->setReady(desc); + } }, + /* read settings, headers and responses from the server */ + { ExpectedStep::ExpectedRequest::readFromBackend, IOState::Done, std::numeric_limits::max() }, + /* acknowledge settings */ + { ExpectedStep::ExpectedRequest::writeToBackend, IOState::Done, std::numeric_limits::max() }, + /* read ends up as a time out since nghttp2 filters the frame with the wrong stream ID */ + { ExpectedStep::ExpectedRequest::readFromBackend, IOState::NeedRead, 0, [&timeout](int desc, const ExpectedStep& step) { + /* set the timeout flag now, since the timeout occurs while waiting for the descriptor to become readable */ + timeout = true; + } }, + { ExpectedStep::ExpectedRequest::closeBackend, IOState::Done }, + }; + + for (auto& query : queries) { + auto sliced = std::static_pointer_cast(query.first); + bool result = sendH2Query(backend, s_mplexer, sliced, std::move(query.second), false); + BOOST_CHECK_EQUAL(result, true); + } + + while (!timeout && (s_mplexer->getWatchedFDCount(false) != 0 || s_mplexer->getWatchedFDCount(true) != 0)) { + s_mplexer->run(&now); + } + + struct timeval later = now; + later.tv_sec += backend->tcpRecvTimeout + 1; + + auto expiredConns = handleH2Timeouts(*s_mplexer, later); + BOOST_CHECK_EQUAL(expiredConns, 1U); + + BOOST_CHECK_EQUAL(queries.at(0).first->d_valid, false); + BOOST_CHECK_EQUAL(queries.at(0).first->d_error, true); + BOOST_CHECK_EQUAL(queries.at(1).first->d_valid, false); + BOOST_CHECK_EQUAL(queries.at(1).first->d_error, true); + + BOOST_CHECK_EQUAL(clearH2Connections(), 0U); +} + +BOOST_AUTO_TEST_SUITE_END(); +#endif /* HAVE_NGHTTP2 */ diff --git a/pdns/dnsdistdist/test-dnsdisttcp_cc.cc b/pdns/dnsdistdist/test-dnsdisttcp_cc.cc index 884780b28c..9c7e1be99f 100644 --- a/pdns/dnsdistdist/test-dnsdisttcp_cc.cc +++ b/pdns/dnsdistdist/test-dnsdisttcp_cc.cc @@ -1477,7 +1477,7 @@ BOOST_AUTO_TEST_CASE(test_IncomingConnection_BackendNoOOOR) auto state = std::make_shared(ConnectionInfo(&localCS), threadData, now); IncomingTCPConnectionState::handleIO(state, now); BOOST_CHECK_EQUAL(s_writeBuffer.size(), 0U); - BOOST_CHECK_EQUAL(s_backendWriteBuffer.size(), query.size() * backend->retries); + BOOST_CHECK_EQUAL(s_backendWriteBuffer.size(), query.size() * backend->d_retries); BOOST_CHECK_EQUAL(backend->outstanding.load(), 0U); /* we need to clear them now, otherwise we end up with dangling pointers to the steps via the TLS context, etc */ @@ -1538,7 +1538,7 @@ BOOST_AUTO_TEST_CASE(test_IncomingConnection_BackendNoOOOR) IncomingTCPConnectionState::handleIO(state, now); BOOST_CHECK_EQUAL(s_writeBuffer.size(), query.size()); BOOST_CHECK(s_writeBuffer == query); - BOOST_CHECK_EQUAL(s_backendWriteBuffer.size(), query.size() * backend->retries); + BOOST_CHECK_EQUAL(s_backendWriteBuffer.size(), query.size() * backend->d_retries); BOOST_CHECK_EQUAL(backend->outstanding.load(), 0U); /* we need to clear them now, otherwise we end up with dangling pointers to the steps via the TLS context, etc */ diff --git a/regression-tests.dnsdist/dnsdisttests.py b/regression-tests.dnsdist/dnsdisttests.py index 083d72ee93..57d3d2ccbf 100644 --- a/regression-tests.dnsdist/dnsdisttests.py +++ b/regression-tests.dnsdist/dnsdisttests.py @@ -486,18 +486,25 @@ class DNSDistTest(AssertEqualDNSMessageMixin, unittest.TestCase): @classmethod def recvTCPResponseOverConnection(cls, sock, useQueue=False, timeout=2.0): + print("reading data") message = None data = sock.recv(2) if data: (datalen,) = struct.unpack("!H", data) + print(datalen) data = sock.recv(datalen) if data: + print(data) message = dns.message.from_wire(data) + print(useQueue) if useQueue and not cls._fromResponderQueue.empty(): receivedQuery = cls._fromResponderQueue.get(True, timeout) + print("Got from queue") + print(receivedQuery) return (receivedQuery, message) else: + print("queue empty") return message @classmethod @@ -519,8 +526,13 @@ class DNSDistTest(AssertEqualDNSMessageMixin, unittest.TestCase): sock.close() receivedQuery = None + print(useQueue) if useQueue and not cls._fromResponderQueue.empty(): + print("Got from queue") + print(receivedQuery) receivedQuery = cls._fromResponderQueue.get(True, timeout) + else: + print("queue is empty") return (receivedQuery, message) diff --git a/regression-tests.dnsdist/test_OutgoingDOH.py b/regression-tests.dnsdist/test_OutgoingDOH.py index 28a579c27f..205518c1bb 100644 --- a/regression-tests.dnsdist/test_OutgoingDOH.py +++ b/regression-tests.dnsdist/test_OutgoingDOH.py @@ -140,7 +140,6 @@ class OutgoingDOHBrokenResponsesTests(object): query = dns.message.make_query(name, 'A', 'IN') (_, receivedResponse) = self.sendUDPQuery(query, response=None, useQueue=False) - print(receivedResponse) self.assertEqual(receivedResponse, None) name = 'invalid-dns-payload.broken-responses.outgoing-doh.test.powerdns.com.' @@ -155,6 +154,48 @@ class OutgoingDOHBrokenResponsesTests(object): (_, receivedResponse) = self.sendUDPQuery(query, response=None, useQueue=False) self.assertEqual(receivedResponse, None) + # but a valid response should be successful + name = 'valid.broken-responses.outgoing-doh.test.powerdns.com.' + query = dns.message.make_query(name, 'A', 'IN') + response = dns.message.make_response(query) + + (_, receivedResponse) = self.sendUDPQuery(query, response) + # we can't check the received query because the responder does not populate the queue.. + # self.assertEqual(query, receivedQuery) + self.assertEqual(response, receivedResponse) + + def testTCP(self): + """ + Outgoing DOH (broken responses): TCP query is sent via DOH + """ + name = '500-status.broken-responses.outgoing-doh.test.powerdns.com.' + query = dns.message.make_query(name, 'A', 'IN') + + (_, receivedResponse) = self.sendTCPQuery(query, response=None, useQueue=False) + self.assertEqual(receivedResponse, None) + + name = 'invalid-dns-payload.broken-responses.outgoing-doh.test.powerdns.com.' + query = dns.message.make_query(name, 'A', 'IN') + + (_, receivedResponse) = self.sendTCPQuery(query, response=None, useQueue=False) + self.assertEqual(receivedResponse, None) + + name = 'closing-connection-id.broken-responses.outgoing-doh.test.powerdns.com.' + query = dns.message.make_query(name, 'A', 'IN') + + (_, receivedResponse) = self.sendTCPQuery(query, response=None, useQueue=False) + self.assertEqual(receivedResponse, None) + + # but a valid response should be successful + name = 'valid.broken-responses.outgoing-doh.test.powerdns.com.' + query = dns.message.make_query(name, 'A', 'IN') + response = dns.message.make_response(query) + + (_, receivedResponse) = self.sendTCPQuery(query, response) + # we can't check the received query because the responder does not populate the queue.. + #self.assertEqual(query, receivedQuery) + self.assertEqual(response, receivedResponse) + class TestOutgoingDOHOpenSSL(DNSDistTest, OutgoingDOHTests): _tlsBackendPort = 10543 _config_params = ['_tlsBackendPort', '_webServerPort', '_webServerBasicAuthPassword', '_webServerAPIKey'] @@ -322,6 +363,7 @@ class TestOutgoingDOHBrokenResponsesGnuTLS(DNSDistTest, OutgoingDOHBrokenRespons webserver("127.0.0.1:%s") setWebserverConfig({password="%s", apiKey="%s"}) """ + _verboseMode = True def callback(request):