From: Charles-Henri Bruyand Date: Fri, 24 Nov 2023 16:59:55 +0000 (+0100) Subject: dnsdist: refactor some common code between doq/doh3 X-Git-Tag: dnsdist-1.9.0-alpha4~15^2~9 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=ed16c81f7b0eb4b64980ccf25d3513e3a582e7aa;p=thirdparty%2Fpdns.git dnsdist: refactor some common code between doq/doh3 --- diff --git a/pdns/dnsdist-lua.cc b/pdns/dnsdist-lua.cc index 8e237c4e22..f20210d210 100644 --- a/pdns/dnsdist-lua.cc +++ b/pdns/dnsdist-lua.cc @@ -59,6 +59,7 @@ #include "base64.hh" #include "coverage.hh" #include "doh.hh" +#include "doq-common.hh" #include "dolog.hh" #include "sodcrypto.hh" #include "threadname.hh" @@ -2597,7 +2598,7 @@ static void setupLuaConfig(LuaContext& luaCtx, bool client, bool configCheck) setLuaSideEffect(); auto frontend = std::make_shared(); - if (!loadTLSCertificateAndKeys("addDOH3Local", frontend->d_tlsConfig.d_certKeyPairs, certFiles, keyFiles)) { + if (!loadTLSCertificateAndKeys("addDOH3Local", frontend->d_quicheParams.d_tlsConfig.d_certKeyPairs, certFiles, keyFiles)) { return; } frontend->d_local = ComboAddress(addr, 853); @@ -2614,23 +2615,23 @@ static void setupLuaConfig(LuaContext& luaCtx, bool client, bool configCheck) if (vars) { parseLocalBindVars(vars, reusePort, tcpFastOpenQueueSize, interface, cpus, tcpListenQueueSize, maxInFlightQueriesPerConn, tcpMaxConcurrentConnections); if (maxInFlightQueriesPerConn > 0) { - frontend->d_maxInFlight = maxInFlightQueriesPerConn; + frontend->d_quicheParams.d_maxInFlight = maxInFlightQueriesPerConn; } getOptionalValue(vars, "internalPipeBufferSize", frontend->d_internalPipeBufferSize); - getOptionalValue(vars, "idleTimeout", frontend->d_idleTimeout); - getOptionalValue(vars, "keyLogFile", frontend->d_keyLogFile); + getOptionalValue(vars, "idleTimeout", frontend->d_quicheParams.d_idleTimeout); + getOptionalValue(vars, "keyLogFile", frontend->d_quicheParams.d_keyLogFile); { std::string valueStr; if (getOptionalValue(vars, "congestionControlAlgo", valueStr) > 0) { - if (DOH3Frontend::s_available_cc_algorithms.count(valueStr) > 0) { - frontend->d_ccAlgo = valueStr; + if (dnsdist::doq::s_available_cc_algorithms.count(valueStr) > 0) { + frontend->d_quicheParams.d_ccAlgo = valueStr; } else { warnlog("Ignoring unknown value '%s' for 'congestionControlAlgo' on 'addDOH3Local'", valueStr); } } } - parseTLSConfig(frontend->d_tlsConfig, "addDOH3Local", vars); + parseTLSConfig(frontend->d_quicheParams.d_tlsConfig, "addDOH3Local", vars); bool ignoreTLSConfigurationErrors = false; if (getOptionalValue(vars, "ignoreTLSConfigurationErrors", ignoreTLSConfigurationErrors) > 0 && ignoreTLSConfigurationErrors) { @@ -2638,7 +2639,7 @@ static void setupLuaConfig(LuaContext& luaCtx, bool client, bool configCheck) // and properly ignore the frontend before actually launching it try { std::map ocspResponses = {}; - auto ctx = libssl_init_server_context(frontend->d_tlsConfig, ocspResponses); + auto ctx = libssl_init_server_context(frontend->d_quicheParams.d_tlsConfig, ocspResponses); } catch (const std::runtime_error& e) { errlog("Ignoring DoH3 frontend: '%s'", e.what()); @@ -2671,7 +2672,7 @@ static void setupLuaConfig(LuaContext& luaCtx, bool client, bool configCheck) setLuaSideEffect(); auto frontend = std::make_shared(); - if (!loadTLSCertificateAndKeys("addDOQLocal", frontend->d_tlsConfig.d_certKeyPairs, certFiles, keyFiles)) { + if (!loadTLSCertificateAndKeys("addDOQLocal", frontend->d_quicheParams.d_tlsConfig.d_certKeyPairs, certFiles, keyFiles)) { return; } frontend->d_local = ComboAddress(addr, 853); @@ -2688,23 +2689,23 @@ static void setupLuaConfig(LuaContext& luaCtx, bool client, bool configCheck) if (vars) { parseLocalBindVars(vars, reusePort, tcpFastOpenQueueSize, interface, cpus, tcpListenQueueSize, maxInFlightQueriesPerConn, tcpMaxConcurrentConnections); if (maxInFlightQueriesPerConn > 0) { - frontend->d_maxInFlight = maxInFlightQueriesPerConn; + frontend->d_quicheParams.d_maxInFlight = maxInFlightQueriesPerConn; } getOptionalValue(vars, "internalPipeBufferSize", frontend->d_internalPipeBufferSize); - getOptionalValue(vars, "idleTimeout", frontend->d_idleTimeout); - getOptionalValue(vars, "keyLogFile", frontend->d_keyLogFile); + getOptionalValue(vars, "idleTimeout", frontend->d_quicheParams.d_idleTimeout); + getOptionalValue(vars, "keyLogFile", frontend->d_quicheParams.d_keyLogFile); { std::string valueStr; if (getOptionalValue(vars, "congestionControlAlgo", valueStr) > 0) { - if (DOQFrontend::s_available_cc_algorithms.count(valueStr) > 0) { - frontend->d_ccAlgo = valueStr; + if (dnsdist::doq::s_available_cc_algorithms.count(valueStr) > 0) { + frontend->d_quicheParams.d_ccAlgo = valueStr; } else { warnlog("Ignoring unknown value '%s' for 'congestionControlAlgo' on 'addDOQLocal'", valueStr); } } } - parseTLSConfig(frontend->d_tlsConfig, "addDOQLocal", vars); + parseTLSConfig(frontend->d_quicheParams.d_tlsConfig, "addDOQLocal", vars); bool ignoreTLSConfigurationErrors = false; if (getOptionalValue(vars, "ignoreTLSConfigurationErrors", ignoreTLSConfigurationErrors) > 0 && ignoreTLSConfigurationErrors) { @@ -2712,7 +2713,7 @@ static void setupLuaConfig(LuaContext& luaCtx, bool client, bool configCheck) // and properly ignore the frontend before actually launching it try { std::map ocspResponses = {}; - auto ctx = libssl_init_server_context(frontend->d_tlsConfig, ocspResponses); + auto ctx = libssl_init_server_context(frontend->d_quicheParams.d_tlsConfig, ocspResponses); } catch (const std::runtime_error& e) { errlog("Ignoring DoQ frontend: '%s'", e.what()); diff --git a/pdns/dnsdistdist/Makefile.am b/pdns/dnsdistdist/Makefile.am index 3effa56b3f..8d3334d35d 100644 --- a/pdns/dnsdistdist/Makefile.am +++ b/pdns/dnsdistdist/Makefile.am @@ -216,6 +216,7 @@ dnsdist_SOURCES = \ doh3.hh \ dolog.cc dolog.hh \ doq.hh \ + doq-common.hh \ ednscookies.cc ednscookies.hh \ ednsextendederror.cc ednsextendederror.hh \ ednsoptions.cc ednsoptions.hh \ @@ -464,6 +465,7 @@ endif if HAVE_QUICHE AM_CPPFLAGS += $(QUICHE_CFLAGS) dnsdist_LDADD += $(QUICHE_LDFLAGS) $(QUICHE_LIBS) +dnsdist_SOURCES += doq-common.cc endif if !HAVE_LUA_HPP diff --git a/pdns/dnsdistdist/doh3.cc b/pdns/dnsdistdist/doh3.cc index 80d07caca9..71593f007d 100644 --- a/pdns/dnsdistdist/doh3.cc +++ b/pdns/dnsdistdist/doh3.cc @@ -40,19 +40,9 @@ #include "dnsdist-tcp.hh" #include "dnsdist-random.hh" -// FIXME : to be renamed ? -static std::string s_quicRetryTokenKey = newKey(false); +#include "doq-common.hh" -std::map DOH3Frontend::s_available_cc_algorithms = { - {"reno", QUICHE_CC_RENO}, - {"cubic", QUICHE_CC_CUBIC}, - {"bbr", QUICHE_CC_BBR}, -}; - -using QuicheConnection = std::unique_ptr; -using QuicheHTTP3Connection = std::unique_ptr; -using QuicheConfig = std::unique_ptr; -using QuicheHTTP3Config = std::unique_ptr; +using namespace dnsdist::doq; class H3Connection { @@ -110,15 +100,6 @@ struct DOH3ServerConfig DOH3Frontend::DOH3Frontend() = default; DOH3Frontend::~DOH3Frontend() = default; -#if 0 -#define DEBUGLOG_ENABLED -#define DEBUGLOG(x) std::cerr << x << std::endl; -#else -#define DEBUGLOG(x) -#endif - -static constexpr size_t MAX_DATAGRAM_SIZE = 1200; -static constexpr size_t LOCAL_CONN_ID_LEN = 16; class DOH3TCPCrossQuerySender final : public TCPQuerySender { @@ -343,75 +324,12 @@ static void handleResponse(DOH3Frontend& frontend, H3Connection& conn, const uin } } -static void fillRandom(PacketBuffer& buffer, size_t size) -{ - buffer.reserve(size); - while (size > 0) { - buffer.insert(buffer.end(), dnsdist::getRandomValue(std::numeric_limits::max())); - --size; - } -} - void DOH3Frontend::setup() { auto config = QuicheConfig(quiche_config_new(QUICHE_PROTOCOL_VERSION), quiche_config_free); - for (const auto& pair : d_tlsConfig.d_certKeyPairs) { - auto res = quiche_config_load_cert_chain_from_pem_file(config.get(), pair.d_cert.c_str()); - if (res != 0) { - throw std::runtime_error("Error loading the server certificate: " + std::to_string(res)); - } - if (pair.d_key) { - res = quiche_config_load_priv_key_from_pem_file(config.get(), pair.d_key->c_str()); - if (res != 0) { - throw std::runtime_error("Error loading the server key: " + std::to_string(res)); - } - } - } - - { - auto res = quiche_config_set_application_protos(config.get(), - (uint8_t*)QUICHE_H3_APPLICATION_PROTOCOL, - sizeof(QUICHE_H3_APPLICATION_PROTOCOL) - 1); - if (res != 0) { - throw std::runtime_error("Error setting ALPN: " + std::to_string(res)); - } - } - quiche_config_set_max_idle_timeout(config.get(), d_idleTimeout * 1000); - /* maximum size of an outgoing packet, which means the buffer we pass to quiche_conn_send() should be at least that big */ - quiche_config_set_max_send_udp_payload_size(config.get(), MAX_DATAGRAM_SIZE); - quiche_config_set_max_recv_udp_payload_size(config.get(), MAX_DATAGRAM_SIZE); - - // The number of concurrent remotely-initiated bidirectional streams to be open at any given time - // https://docs.rs/quiche/latest/quiche/struct.Config.html#method.set_initial_max_streams_bidi - // 0 means none will get accepted, that's why we have a default value of 65535 - quiche_config_set_initial_max_streams_bidi(config.get(), d_maxInFlight); - quiche_config_set_initial_max_streams_uni(config.get(), d_maxInFlight); - - // The number of bytes of incoming stream data to be buffered for each localy or remotely-initiated bidirectional stream - quiche_config_set_initial_max_stream_data_bidi_local(config.get(), 1000000); - quiche_config_set_initial_max_stream_data_bidi_remote(config.get(), 1000000); - quiche_config_set_initial_max_stream_data_uni(config.get(), 1000000); - - quiche_config_set_disable_active_migration(config.get(), true); - - // The number of total bytes of incoming stream data to be buffered for the whole connection - // https://docs.rs/quiche/latest/quiche/struct.Config.html#method.set_initial_max_data - quiche_config_set_initial_max_data(config.get(), 8192 * d_maxInFlight); - if (!d_keyLogFile.empty()) { - quiche_config_log_keys(config.get()); - } - - auto algo = DOH3Frontend::s_available_cc_algorithms.find(d_ccAlgo); - if (algo != DOH3Frontend::s_available_cc_algorithms.end()) { - quiche_config_set_cc_algorithm(config.get(), static_cast(algo->second)); - } - - { - PacketBuffer resetToken; - fillRandom(resetToken, 16); - quiche_config_set_stateless_reset_token(config.get(), resetToken.data()); - } + d_quicheParams.d_alpn = std::string(DOH3_ALPN.begin(), DOH3_ALPN.end()); + configureQuiche(config, d_quicheParams); // quiche_h3_config_new auto http3config = QuicheHTTP3Config(quiche_h3_config_new(), quiche_h3_config_free); @@ -419,127 +337,6 @@ void DOH3Frontend::setup() d_server_config = std::make_unique(std::move(config), std::move(http3config), d_internalPipeBufferSize); } -static std::optional getCID() -{ - PacketBuffer buffer; - - fillRandom(buffer, LOCAL_CONN_ID_LEN); - - return buffer; -} - -static constexpr size_t MAX_TOKEN_LEN = dnsdist::crypto::authenticated::getEncryptedSize(std::tuple_size{} /* nonce */ + sizeof(uint64_t) /* TTD */ + 16 /* IPv6 */ + QUICHE_MAX_CONN_ID_LEN); - -static PacketBuffer mintToken(const PacketBuffer& dcid, const ComboAddress& peer) -{ - try { - SodiumNonce nonce; - nonce.init(); - - const auto addrBytes = peer.toByteString(); - // this token will be valid for 60s - const uint64_t ttd = time(nullptr) + 60U; - PacketBuffer plainTextToken; - plainTextToken.reserve(sizeof(ttd) + addrBytes.size() + dcid.size()); - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast,cppcoreguidelines-pro-bounds-pointer-arithmetic) - plainTextToken.insert(plainTextToken.end(), reinterpret_cast(&ttd), reinterpret_cast(&ttd) + sizeof(ttd)); - plainTextToken.insert(plainTextToken.end(), addrBytes.begin(), addrBytes.end()); - plainTextToken.insert(plainTextToken.end(), dcid.begin(), dcid.end()); - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) - const auto encryptedToken = sodEncryptSym(std::string_view(reinterpret_cast(plainTextToken.data()), plainTextToken.size()), s_quicRetryTokenKey, nonce, false); - // a bit sad, let's see if we can do better later - auto encryptedTokenPacket = PacketBuffer(encryptedToken.begin(), encryptedToken.end()); - encryptedTokenPacket.insert(encryptedTokenPacket.begin(), nonce.value.begin(), nonce.value.end()); - return encryptedTokenPacket; - } - catch (const std::exception& exp) { - vinfolog("Error while minting DoH3 token: %s", exp.what()); - throw; - } -} - -// returns the original destination ID if the token is valid, nothing otherwise -static std::optional validateToken(const PacketBuffer& token, const ComboAddress& peer) -{ - try { - SodiumNonce nonce; - auto addrBytes = peer.toByteString(); - const uint64_t now = time(nullptr); - const auto minimumSize = nonce.value.size() + sizeof(now) + addrBytes.size(); - if (token.size() <= minimumSize) { - return std::nullopt; - } - - memcpy(nonce.value.data(), token.data(), nonce.value.size()); - - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) - auto cipher = std::string_view(reinterpret_cast(&token.at(nonce.value.size())), token.size() - nonce.value.size()); - auto plainText = sodDecryptSym(cipher, s_quicRetryTokenKey, nonce, false); - - if (plainText.size() <= sizeof(now) + addrBytes.size()) { - return std::nullopt; - } - - uint64_t ttd{0}; - memcpy(&ttd, plainText.data(), sizeof(ttd)); - if (ttd < now) { - return std::nullopt; - } - - if (std::memcmp(&plainText.at(sizeof(ttd)), &*addrBytes.begin(), addrBytes.size()) != 0) { - return std::nullopt; - } - // NOLINTNEXTLINE(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions) - return PacketBuffer(plainText.begin() + (sizeof(ttd) + addrBytes.size()), plainText.end()); - } - catch (const std::exception& exp) { - vinfolog("Error while validating DoH3 token: %s", exp.what()); - return std::nullopt; - } -} - -static void handleStatelessRetry(Socket& sock, const PacketBuffer& clientConnID, const PacketBuffer& serverConnID, const ComboAddress& peer, uint32_t version) -{ - auto newServerConnID = getCID(); - if (!newServerConnID) { - return; - } - - auto token = mintToken(serverConnID, peer); - - PacketBuffer out(MAX_DATAGRAM_SIZE); - auto written = quiche_retry(clientConnID.data(), clientConnID.size(), - serverConnID.data(), serverConnID.size(), - newServerConnID->data(), newServerConnID->size(), - token.data(), token.size(), - version, - out.data(), out.size()); - - if (written < 0) { - DEBUGLOG("failed to create retry packet " << written); - return; - } - - out.resize(written); - sock.sendTo(std::string(out.begin(), out.end()), peer); -} - -static void handleVersionNegociation(Socket& sock, const PacketBuffer& clientConnID, const PacketBuffer& serverConnID, const ComboAddress& peer) -{ - PacketBuffer out(MAX_DATAGRAM_SIZE); - - auto written = quiche_negotiate_version(clientConnID.data(), clientConnID.size(), - serverConnID.data(), serverConnID.size(), - out.data(), out.size()); - - if (written < 0) { - DEBUGLOG("failed to create vneg packet " << written); - return; - } - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) - sock.sendTo(reinterpret_cast(out.data()), written, peer); -} - static std::optional> getConnection(DOH3ServerConfig::ConnectionsMap& connMap, const PacketBuffer& connID) { auto iter = connMap.find(connID); @@ -578,8 +375,8 @@ static std::optional> createConnection(DOH3 config.config.get()), quiche_conn_free); - if (config.df && !config.df->d_keyLogFile.empty()) { - quiche_conn_set_keylog_path(quicheConn.get(), config.df->d_keyLogFile.c_str()); + if (config.df && !config.df->d_quicheParams.d_keyLogFile.empty()) { + quiche_conn_set_keylog_path(quicheConn.get(), config.df->d_quicheParams.d_keyLogFile.c_str()); } auto conn = H3Connection(peer, std::move(quicheConn)); @@ -587,26 +384,6 @@ static std::optional> createConnection(DOH3 return pair.first->second; } -static void flushEgress(Socket& sock, H3Connection& conn) -{ - std::array out{}; - quiche_send_info send_info; - - while (true) { - auto written = quiche_conn_send(conn.d_conn.get(), out.data(), out.size(), &send_info); - if (written == QUICHE_ERR_DONE) { - return; - } - - if (written < 0) { - return; - } - // FIXME pacing (as send_info.at should tell us when to send the packet) ? - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) - sock.sendTo(reinterpret_cast(out.data()), written, conn.d_peer); - } -} - std::unique_ptr getDOH3CrossProtocolQueryFromDQ(DNSQuestion& dnsQuestion, bool isResponse) { if (!dnsQuestion.ids.doh3u) { @@ -950,7 +727,7 @@ void doh3Thread(ClientState* clientState) DEBUGLOG("Successfully created HTTP/3 connection"); } - while (1) { + while (true) { quiche_h3_event* ev; // Processes HTTP/3 data received from the peer int64_t streamID = quiche_h3_conn_poll(conn->get().d_http3.get(), @@ -1075,7 +852,7 @@ void doh3Thread(ClientState* clientState) for (auto conn = frontend->d_server_config->d_connections.begin(); conn != frontend->d_server_config->d_connections.end();) { quiche_conn_on_timeout(conn->second.d_conn.get()); - flushEgress(sock, conn->second); + flushEgress(sock, conn->second.d_conn, conn->second.d_peer); if (quiche_conn_is_closed(conn->second.d_conn.get())) { #ifdef DEBUGLOG_ENABLED diff --git a/pdns/dnsdistdist/doh3.hh b/pdns/dnsdistdist/doh3.hh index 40d44b9289..bca7a4eb52 100644 --- a/pdns/dnsdistdist/doh3.hh +++ b/pdns/dnsdistdist/doh3.hh @@ -36,6 +36,8 @@ struct DownstreamState; #ifdef HAVE_DNS_OVER_HTTP3 +#include "doq-common.hh" + struct DOH3Frontend { DOH3Frontend(); @@ -48,9 +50,7 @@ struct DOH3Frontend void setup(); std::unique_ptr d_server_config; - TLSConfig d_tlsConfig; ComboAddress d_local; - std::string d_keyLogFile; #ifdef __linux__ // On Linux this gives us 128k pending queries (default is 8192 queries), @@ -59,16 +59,12 @@ struct DOH3Frontend #else uint32_t d_internalPipeBufferSize{0}; #endif - uint64_t d_idleTimeout{5}; - uint64_t d_maxInFlight{65535}; - std::string d_ccAlgo{"reno"}; + dnsdist::doq::QuicheParams d_quicheParams; pdns::stat_t d_doh3UnsupportedVersionErrors{0}; // Unsupported protocol version errors pdns::stat_t d_doh3InvalidTokensReceived{0}; // Discarded received tokens pdns::stat_t d_validResponses{0}; // Valid responses sent pdns::stat_t d_errorResponses{0}; // Empty responses (no backend, drops, invalid queries, etc.) - - static std::map s_available_cc_algorithms; }; struct DOH3Unit diff --git a/pdns/dnsdistdist/doq-common.cc b/pdns/dnsdistdist/doq-common.cc new file mode 100644 index 0000000000..c06ef49622 --- /dev/null +++ b/pdns/dnsdistdist/doq-common.cc @@ -0,0 +1,240 @@ +/* + * 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. + */ + +#include "doq-common.hh" +#include "dnsdist-random.hh" +#include "libssl.hh" + +#ifdef HAVE_DNS_OVER_QUIC + +namespace dnsdist::doq { + +static const std::string s_quicRetryTokenKey = newKey(false); + +PacketBuffer mintToken(const PacketBuffer& dcid, const ComboAddress& peer) +{ + try { + SodiumNonce nonce; + nonce.init(); + + const auto addrBytes = peer.toByteString(); + // this token will be valid for 60s + const uint64_t ttd = time(nullptr) + 60U; + PacketBuffer plainTextToken; + plainTextToken.reserve(sizeof(ttd) + addrBytes.size() + dcid.size()); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast,cppcoreguidelines-pro-bounds-pointer-arithmetic) + plainTextToken.insert(plainTextToken.end(), reinterpret_cast(&ttd), reinterpret_cast(&ttd) + sizeof(ttd)); + plainTextToken.insert(plainTextToken.end(), addrBytes.begin(), addrBytes.end()); + plainTextToken.insert(plainTextToken.end(), dcid.begin(), dcid.end()); + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + const auto encryptedToken = sodEncryptSym(std::string_view(reinterpret_cast(plainTextToken.data()), plainTextToken.size()), s_quicRetryTokenKey, nonce, false); + // a bit sad, let's see if we can do better later + auto encryptedTokenPacket = PacketBuffer(encryptedToken.begin(), encryptedToken.end()); + encryptedTokenPacket.insert(encryptedTokenPacket.begin(), nonce.value.begin(), nonce.value.end()); + return encryptedTokenPacket; + } + catch (const std::exception& exp) { + vinfolog("Error while minting DoH3 token: %s", exp.what()); + throw; + } +} + +void fillRandom(PacketBuffer& buffer, size_t size) +{ + buffer.reserve(size); + while (size > 0) { + buffer.insert(buffer.end(), dnsdist::getRandomValue(std::numeric_limits::max())); + --size; + } +} + +std::optional getCID() +{ + PacketBuffer buffer; + + fillRandom(buffer, LOCAL_CONN_ID_LEN); + + return buffer; +} + +// returns the original destination ID if the token is valid, nothing otherwise +std::optional validateToken(const PacketBuffer& token, const ComboAddress& peer) +{ + try { + SodiumNonce nonce; + auto addrBytes = peer.toByteString(); + const uint64_t now = time(nullptr); + const auto minimumSize = nonce.value.size() + sizeof(now) + addrBytes.size(); + if (token.size() <= minimumSize) { + return std::nullopt; + } + + memcpy(nonce.value.data(), token.data(), nonce.value.size()); + + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + auto cipher = std::string_view(reinterpret_cast(&token.at(nonce.value.size())), token.size() - nonce.value.size()); + auto plainText = sodDecryptSym(cipher, s_quicRetryTokenKey, nonce, false); + + if (plainText.size() <= sizeof(now) + addrBytes.size()) { + return std::nullopt; + } + + uint64_t ttd{0}; + memcpy(&ttd, plainText.data(), sizeof(ttd)); + if (ttd < now) { + return std::nullopt; + } + + if (std::memcmp(&plainText.at(sizeof(ttd)), &*addrBytes.begin(), addrBytes.size()) != 0) { + return std::nullopt; + } + // NOLINTNEXTLINE(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions) + return PacketBuffer(plainText.begin() + (sizeof(ttd) + addrBytes.size()), plainText.end()); + } + catch (const std::exception& exp) { + vinfolog("Error while validating DoH3 token: %s", exp.what()); + return std::nullopt; + } +} + +void handleStatelessRetry(Socket& sock, const PacketBuffer& clientConnID, const PacketBuffer& serverConnID, const ComboAddress& peer, uint32_t version) +{ + auto newServerConnID = getCID(); + if (!newServerConnID) { + return; + } + + auto token = mintToken(serverConnID, peer); + + PacketBuffer out(MAX_DATAGRAM_SIZE); + auto written = quiche_retry(clientConnID.data(), clientConnID.size(), + serverConnID.data(), serverConnID.size(), + newServerConnID->data(), newServerConnID->size(), + token.data(), token.size(), + version, + out.data(), out.size()); + + if (written < 0) { + DEBUGLOG("failed to create retry packet " << written); + return; + } + + out.resize(written); + sock.sendTo(std::string(out.begin(), out.end()), peer); +} + +void handleVersionNegociation(Socket& sock, const PacketBuffer& clientConnID, const PacketBuffer& serverConnID, const ComboAddress& peer) +{ + PacketBuffer out(MAX_DATAGRAM_SIZE); + + auto written = quiche_negotiate_version(clientConnID.data(), clientConnID.size(), + serverConnID.data(), serverConnID.size(), + out.data(), out.size()); + + if (written < 0) { + DEBUGLOG("failed to create vneg packet " << written); + return; + } + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + sock.sendTo(reinterpret_cast(out.data()), written, peer); +} + +void flushEgress(Socket& sock, QuicheConnection& conn, const ComboAddress& peer) +{ + std::array out{}; + quiche_send_info send_info; + + while (true) { + auto written = quiche_conn_send(conn.get(), out.data(), out.size(), &send_info); + if (written == QUICHE_ERR_DONE) { + return; + } + + if (written < 0) { + return; + } + // FIXME pacing (as send_info.at should tell us when to send the packet) ? + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + sock.sendTo(reinterpret_cast(out.data()), written, peer); + } +} + +void configureQuiche(QuicheConfig& config, const QuicheParams& params) +{ + for (const auto& pair : params.d_tlsConfig.d_certKeyPairs) { + auto res = quiche_config_load_cert_chain_from_pem_file(config.get(), pair.d_cert.c_str()); + if (res != 0) { + throw std::runtime_error("Error loading the server certificate: " + std::to_string(res)); + } + if (pair.d_key) { + res = quiche_config_load_priv_key_from_pem_file(config.get(), pair.d_key->c_str()); + if (res != 0) { + throw std::runtime_error("Error loading the server key: " + std::to_string(res)); + } + } + } + + { + auto res = quiche_config_set_application_protos(config.get(), + reinterpret_cast(params.d_alpn.data()), + params.d_alpn.size()); + if (res != 0) { + throw std::runtime_error("Error setting ALPN: " + std::to_string(res)); + } + } + + quiche_config_set_max_idle_timeout(config.get(), params.d_idleTimeout * 1000); + /* maximum size of an outgoing packet, which means the buffer we pass to quiche_conn_send() should be at least that big */ + quiche_config_set_max_send_udp_payload_size(config.get(), MAX_DATAGRAM_SIZE); + quiche_config_set_max_recv_udp_payload_size(config.get(), MAX_DATAGRAM_SIZE); + + // The number of concurrent remotely-initiated bidirectional streams to be open at any given time + // https://docs.rs/quiche/latest/quiche/struct.Config.html#method.set_initial_max_streams_bidi + // 0 means none will get accepted, that's why we have a default value of 65535 + quiche_config_set_initial_max_streams_bidi(config.get(), params.d_maxInFlight); + + // The number of bytes of incoming stream data to be buffered for each localy or remotely-initiated bidirectional stream + quiche_config_set_initial_max_stream_data_bidi_local(config.get(), 8192); + quiche_config_set_initial_max_stream_data_bidi_remote(config.get(), 8192); + + // The number of total bytes of incoming stream data to be buffered for the whole connection + // https://docs.rs/quiche/latest/quiche/struct.Config.html#method.set_initial_max_data + quiche_config_set_initial_max_data(config.get(), 8192 * params.d_maxInFlight); + if (!params.d_keyLogFile.empty()) { + quiche_config_log_keys(config.get()); + } + + auto algo = dnsdist::doq::s_available_cc_algorithms.find(params.d_ccAlgo); + if (algo != dnsdist::doq::s_available_cc_algorithms.end()) { + quiche_config_set_cc_algorithm(config.get(), static_cast(algo->second)); + } + + { + PacketBuffer resetToken; + fillRandom(resetToken, 16); + quiche_config_set_stateless_reset_token(config.get(), resetToken.data()); + } +} + +}; + +#endif diff --git a/pdns/dnsdistdist/doq-common.hh b/pdns/dnsdistdist/doq-common.hh new file mode 100644 index 0000000000..53552c34fe --- /dev/null +++ b/pdns/dnsdistdist/doq-common.hh @@ -0,0 +1,99 @@ +/* + * 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. + */ +#pragma once + +#include +#include + +#include "config.h" + +#if defined(HAVE_DNS_OVER_QUIC) || defined(HAVE_DNS_OVER_HTTP3) + +#include + +#include "dolog.hh" +#include "noinitvector.hh" +#include "sodcrypto.hh" +#include "sstuff.hh" +#include "libssl.hh" + +#if 0 +#define DEBUGLOG_ENABLED +#define DEBUGLOG(x) std::cerr << x << std::endl; +#else +#define DEBUGLOG(x) +#endif + +namespace dnsdist::doq +{ + +static std::map s_available_cc_algorithms = { + {"reno", QUICHE_CC_RENO}, + {"cubic", QUICHE_CC_CUBIC}, + {"bbr", QUICHE_CC_BBR}, +}; + +using QuicheConnection = std::unique_ptr; +using QuicheHTTP3Connection = std::unique_ptr; +using QuicheConfig = std::unique_ptr; +using QuicheHTTP3Config = std::unique_ptr; + +struct QuicheParams +{ + TLSConfig d_tlsConfig; + std::string d_keyLogFile; + uint64_t d_idleTimeout{5}; + uint64_t d_maxInFlight{65535}; + std::string d_ccAlgo{"reno"}; + std::string d_alpn; +}; + +/* from rfc9250 section-4.3 */ +enum class DOQ_Error_Codes : uint64_t +{ + DOQ_NO_ERROR = 0, + DOQ_INTERNAL_ERROR = 1, + DOQ_PROTOCOL_ERROR = 2, + DOQ_REQUEST_CANCELLED = 3, + DOQ_EXCESSIVE_LOAD = 4, + DOQ_UNSPECIFIED_ERROR = 5 +}; + +static constexpr size_t MAX_TOKEN_LEN = dnsdist::crypto::authenticated::getEncryptedSize(std::tuple_size{} /* nonce */ + sizeof(uint64_t) /* TTD */ + 16 /* IPv6 */ + QUICHE_MAX_CONN_ID_LEN); +static constexpr size_t MAX_DATAGRAM_SIZE = 1200; +static constexpr size_t LOCAL_CONN_ID_LEN = 16; +static constexpr std::array DOQ_ALPN{'\x03', 'd', 'o', 'q'}; +static constexpr std::array DOH3_ALPN{'\x02', 'h', '3'}; + + +void fillRandom(PacketBuffer& buffer, size_t size); +std::optional getCID(); +PacketBuffer mintToken(const PacketBuffer& dcid, const ComboAddress& peer); +std::optional validateToken(const PacketBuffer& token, const ComboAddress& peer); +void handleStatelessRetry(Socket& sock, const PacketBuffer& clientConnID, const PacketBuffer& serverConnID, const ComboAddress& peer, uint32_t version); +void handleVersionNegociation(Socket& sock, const PacketBuffer& clientConnID, const PacketBuffer& serverConnID, const ComboAddress& peer); +void flushEgress(Socket& sock, QuicheConnection& conn, const ComboAddress& peer); +void configureQuiche(QuicheConfig& config, const QuicheParams& params); + +}; + +#endif diff --git a/pdns/dnsdistdist/doq.cc b/pdns/dnsdistdist/doq.cc index 51b1f32a08..7037349652 100644 --- a/pdns/dnsdistdist/doq.cc +++ b/pdns/dnsdistdist/doq.cc @@ -39,16 +39,9 @@ #include "dnsdist-tcp.hh" #include "dnsdist-random.hh" -static std::string s_quicRetryTokenKey = newKey(false); +#include "doq-common.hh" -std::map DOQFrontend::s_available_cc_algorithms = { - {"reno", QUICHE_CC_RENO}, - {"cubic", QUICHE_CC_CUBIC}, - {"bbr", QUICHE_CC_BBR}, -}; - -using QuicheConnection = std::unique_ptr; -using QuicheConfig = std::unique_ptr; +using namespace dnsdist::doq; class Connection { @@ -104,16 +97,6 @@ struct DOQServerConfig DOQFrontend::DOQFrontend() = default; DOQFrontend::~DOQFrontend() = default; -#if 0 -#define DEBUGLOG_ENABLED -#define DEBUGLOG(x) std::cerr << x << std::endl; -#else -#define DEBUGLOG(x) -#endif - -static constexpr size_t MAX_DATAGRAM_SIZE = 1200; -static constexpr size_t LOCAL_CONN_ID_LEN = 16; - class DOQTCPCrossQuerySender final : public TCPQuerySender { public: @@ -303,196 +286,14 @@ static void handleResponse(DOQFrontend& frontend, Connection& conn, const uint64 } } -static void fillRandom(PacketBuffer& buffer, size_t size) -{ - buffer.reserve(size); - while (size > 0) { - buffer.insert(buffer.end(), dnsdist::getRandomValue(std::numeric_limits::max())); - --size; - } -} - void DOQFrontend::setup() { auto config = QuicheConfig(quiche_config_new(QUICHE_PROTOCOL_VERSION), quiche_config_free); - for (const auto& pair : d_tlsConfig.d_certKeyPairs) { - auto res = quiche_config_load_cert_chain_from_pem_file(config.get(), pair.d_cert.c_str()); - if (res != 0) { - throw std::runtime_error("Error loading the server certificate: " + std::to_string(res)); - } - if (pair.d_key) { - res = quiche_config_load_priv_key_from_pem_file(config.get(), pair.d_key->c_str()); - if (res != 0) { - throw std::runtime_error("Error loading the server key: " + std::to_string(res)); - } - } - } - - { - constexpr std::array alpn{'\x03', 'd', 'o', 'q'}; - auto res = quiche_config_set_application_protos(config.get(), - alpn.data(), - alpn.size()); - if (res != 0) { - throw std::runtime_error("Error setting ALPN: " + std::to_string(res)); - } - } - - quiche_config_set_max_idle_timeout(config.get(), d_idleTimeout * 1000); - /* maximum size of an outgoing packet, which means the buffer we pass to quiche_conn_send() should be at least that big */ - quiche_config_set_max_send_udp_payload_size(config.get(), MAX_DATAGRAM_SIZE); - - // The number of concurrent remotely-initiated bidirectional streams to be open at any given time - // https://docs.rs/quiche/latest/quiche/struct.Config.html#method.set_initial_max_streams_bidi - // 0 means none will get accepted, that's why we have a default value of 65535 - quiche_config_set_initial_max_streams_bidi(config.get(), d_maxInFlight); - - // The number of bytes of incoming stream data to be buffered for each localy or remotely-initiated bidirectional stream - quiche_config_set_initial_max_stream_data_bidi_local(config.get(), 8192); - quiche_config_set_initial_max_stream_data_bidi_remote(config.get(), 8192); - - // The number of total bytes of incoming stream data to be buffered for the whole connection - // https://docs.rs/quiche/latest/quiche/struct.Config.html#method.set_initial_max_data - quiche_config_set_initial_max_data(config.get(), 8192 * d_maxInFlight); - if (!d_keyLogFile.empty()) { - quiche_config_log_keys(config.get()); - } - - auto algo = DOQFrontend::s_available_cc_algorithms.find(d_ccAlgo); - if (algo != DOQFrontend::s_available_cc_algorithms.end()) { - quiche_config_set_cc_algorithm(config.get(), static_cast(algo->second)); - } - - { - PacketBuffer resetToken; - fillRandom(resetToken, 16); - quiche_config_set_stateless_reset_token(config.get(), resetToken.data()); - } - + d_quicheParams.d_alpn = std::string(DOQ_ALPN.begin(), DOQ_ALPN.end()); + configureQuiche(config, d_quicheParams); d_server_config = std::make_unique(std::move(config), d_internalPipeBufferSize); } -static std::optional getCID() -{ - PacketBuffer buffer; - - fillRandom(buffer, LOCAL_CONN_ID_LEN); - - return buffer; -} - -static constexpr size_t MAX_TOKEN_LEN = dnsdist::crypto::authenticated::getEncryptedSize(std::tuple_size{} /* nonce */ + sizeof(uint64_t) /* TTD */ + 16 /* IPv6 */ + QUICHE_MAX_CONN_ID_LEN); - -static PacketBuffer mintToken(const PacketBuffer& dcid, const ComboAddress& peer) -{ - try { - SodiumNonce nonce; - nonce.init(); - - const auto addrBytes = peer.toByteString(); - // this token will be valid for 60s - const uint64_t ttd = time(nullptr) + 60U; - PacketBuffer plainTextToken; - plainTextToken.reserve(sizeof(ttd) + addrBytes.size() + dcid.size()); - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast,cppcoreguidelines-pro-bounds-pointer-arithmetic) - plainTextToken.insert(plainTextToken.end(), reinterpret_cast(&ttd), reinterpret_cast(&ttd) + sizeof(ttd)); - plainTextToken.insert(plainTextToken.end(), addrBytes.begin(), addrBytes.end()); - plainTextToken.insert(plainTextToken.end(), dcid.begin(), dcid.end()); - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) - const auto encryptedToken = sodEncryptSym(std::string_view(reinterpret_cast(plainTextToken.data()), plainTextToken.size()), s_quicRetryTokenKey, nonce, false); - // a bit sad, let's see if we can do better later - auto encryptedTokenPacket = PacketBuffer(encryptedToken.begin(), encryptedToken.end()); - encryptedTokenPacket.insert(encryptedTokenPacket.begin(), nonce.value.begin(), nonce.value.end()); - return encryptedTokenPacket; - } - catch (const std::exception& exp) { - vinfolog("Error while minting DoQ token: %s", exp.what()); - throw; - } -} - -// returns the original destination ID if the token is valid, nothing otherwise -static std::optional validateToken(const PacketBuffer& token, const ComboAddress& peer) -{ - try { - SodiumNonce nonce; - auto addrBytes = peer.toByteString(); - const uint64_t now = time(nullptr); - const auto minimumSize = nonce.value.size() + sizeof(now) + addrBytes.size(); - if (token.size() <= minimumSize) { - return std::nullopt; - } - - memcpy(nonce.value.data(), token.data(), nonce.value.size()); - - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) - auto cipher = std::string_view(reinterpret_cast(&token.at(nonce.value.size())), token.size() - nonce.value.size()); - auto plainText = sodDecryptSym(cipher, s_quicRetryTokenKey, nonce, false); - - if (plainText.size() <= sizeof(now) + addrBytes.size()) { - return std::nullopt; - } - - uint64_t ttd{0}; - memcpy(&ttd, plainText.data(), sizeof(ttd)); - if (ttd < now) { - return std::nullopt; - } - - if (std::memcmp(&plainText.at(sizeof(ttd)), &*addrBytes.begin(), addrBytes.size()) != 0) { - return std::nullopt; - } - // NOLINTNEXTLINE(bugprone-narrowing-conversions,cppcoreguidelines-narrowing-conversions) - return PacketBuffer(plainText.begin() + (sizeof(ttd) + addrBytes.size()), plainText.end()); - } - catch (const std::exception& exp) { - vinfolog("Error while validating DoQ token: %s", exp.what()); - return std::nullopt; - } -} - -static void handleStatelessRetry(Socket& sock, const PacketBuffer& clientConnID, const PacketBuffer& serverConnID, const ComboAddress& peer, uint32_t version) -{ - auto newServerConnID = getCID(); - if (!newServerConnID) { - return; - } - - auto token = mintToken(serverConnID, peer); - - PacketBuffer out(MAX_DATAGRAM_SIZE); - auto written = quiche_retry(clientConnID.data(), clientConnID.size(), - serverConnID.data(), serverConnID.size(), - newServerConnID->data(), newServerConnID->size(), - token.data(), token.size(), - version, - out.data(), out.size()); - - if (written < 0) { - DEBUGLOG("failed to create retry packet " << written); - return; - } - - out.resize(written); - sock.sendTo(std::string(out.begin(), out.end()), peer); -} - -static void handleVersionNegociation(Socket& sock, const PacketBuffer& clientConnID, const PacketBuffer& serverConnID, const ComboAddress& peer) -{ - PacketBuffer out(MAX_DATAGRAM_SIZE); - - auto written = quiche_negotiate_version(clientConnID.data(), clientConnID.size(), - serverConnID.data(), serverConnID.size(), - out.data(), out.size()); - - if (written < 0) { - DEBUGLOG("failed to create vneg packet " << written); - return; - } - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) - sock.sendTo(reinterpret_cast(out.data()), written, peer); -} - static std::optional> getConnection(DOQServerConfig::ConnectionsMap& connMap, const PacketBuffer& connID) { auto iter = connMap.find(connID); @@ -531,8 +332,8 @@ static std::optional> createConnection(DOQSer config.config.get()), quiche_conn_free); - if (config.df && !config.df->d_keyLogFile.empty()) { - quiche_conn_set_keylog_path(quicheConn.get(), config.df->d_keyLogFile.c_str()); + if (config.df && !config.df->d_quicheParams.d_keyLogFile.empty()) { + quiche_conn_set_keylog_path(quicheConn.get(), config.df->d_quicheParams.d_keyLogFile.c_str()); } auto conn = Connection(peer, std::move(quicheConn)); @@ -540,26 +341,6 @@ static std::optional> createConnection(DOQSer return pair.first->second; } -static void flushEgress(Socket& sock, Connection& conn) -{ - std::array out{}; - quiche_send_info send_info; - - while (true) { - auto written = quiche_conn_send(conn.d_conn.get(), out.data(), out.size(), &send_info); - if (written == QUICHE_ERR_DONE) { - return; - } - - if (written < 0) { - return; - } - // FIXME pacing (as send_info.at should tell us when to send the packet) ? - // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) - sock.sendTo(reinterpret_cast(out.data()), written, conn.d_peer); - } -} - std::unique_ptr getDOQCrossProtocolQueryFromDQ(DNSQuestion& dnsQuestion, bool isResponse) { if (!dnsQuestion.ids.doqu) { @@ -929,7 +710,7 @@ void doqThread(ClientState* clientState) for (auto conn = frontend->d_server_config->d_connections.begin(); conn != frontend->d_server_config->d_connections.end();) { quiche_conn_on_timeout(conn->second.d_conn.get()); - flushEgress(sock, conn->second); + flushEgress(sock, conn->second.d_conn, conn->second.d_peer); if (quiche_conn_is_closed(conn->second.d_conn.get())) { #ifdef DEBUGLOG_ENABLED diff --git a/pdns/dnsdistdist/doq.hh b/pdns/dnsdistdist/doq.hh index efc50ef218..a61af71a97 100644 --- a/pdns/dnsdistdist/doq.hh +++ b/pdns/dnsdistdist/doq.hh @@ -37,16 +37,7 @@ struct DownstreamState; #ifdef HAVE_DNS_OVER_QUIC -/* from rfc9250 section-4.3 */ -enum class DOQ_Error_Codes : uint64_t -{ - DOQ_NO_ERROR = 0, - DOQ_INTERNAL_ERROR = 1, - DOQ_PROTOCOL_ERROR = 2, - DOQ_REQUEST_CANCELLED = 3, - DOQ_EXCESSIVE_LOAD = 4, - DOQ_UNSPECIFIED_ERROR = 5 -}; +#include "doq-common.hh" struct DOQFrontend { @@ -60,9 +51,8 @@ struct DOQFrontend void setup(); std::unique_ptr d_server_config; - TLSConfig d_tlsConfig; + dnsdist::doq::QuicheParams d_quicheParams; ComboAddress d_local; - std::string d_keyLogFile; #ifdef __linux__ // On Linux this gives us 128k pending queries (default is 8192 queries), @@ -71,16 +61,11 @@ struct DOQFrontend #else uint32_t d_internalPipeBufferSize{0}; #endif - uint64_t d_idleTimeout{5}; - uint64_t d_maxInFlight{65535}; - std::string d_ccAlgo{"reno"}; pdns::stat_t d_doqUnsupportedVersionErrors{0}; // Unsupported protocol version errors pdns::stat_t d_doqInvalidTokensReceived{0}; // Discarded received tokens pdns::stat_t d_validResponses{0}; // Valid responses sent pdns::stat_t d_errorResponses{0}; // Empty responses (no backend, drops, invalid queries, etc.) - - static std::map s_available_cc_algorithms; }; struct DOQUnit