From bc282425088455198a7a99511c75914477d4ed32 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 13 Apr 2024 23:14:51 +0200 Subject: Merging upstream version 1.9.3. Signed-off-by: Daniel Baumann --- doq-common.cc | 263 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 263 insertions(+) create mode 100644 doq-common.cc (limited to 'doq-common.cc') diff --git a/doq-common.cc b/doq-common.cc new file mode 100644 index 0000000..e92ccff --- /dev/null +++ b/doq-common.cc @@ -0,0 +1,263 @@ +/* + * 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 + +#if 0 +#define DEBUGLOG_ENABLED +#define DEBUGLOG(x) std::cerr << x << std::endl; +#else +#define DEBUGLOG(x) +#endif + +namespace dnsdist::doq +{ + +static const std::string s_quicRetryTokenKey = dnsdist::crypto::authenticated::newKey(false); + +PacketBuffer mintToken(const PacketBuffer& dcid, const ComboAddress& peer) +{ + try { + dnsdist::crypto::authenticated::Nonce 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 = dnsdist::crypto::authenticated::encryptSym(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 + PacketBuffer encryptedTokenPacket; + encryptedTokenPacket.reserve(encryptedToken.size() + nonce.value.size()); + encryptedTokenPacket.insert(encryptedTokenPacket.begin(), 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 { + dnsdist::crypto::authenticated::Nonce 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 = dnsdist::crypto::authenticated::decryptSym(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, PacketBuffer& buffer) +{ + auto newServerConnID = getCID(); + if (!newServerConnID) { + return; + } + + auto token = mintToken(serverConnID, peer); + + buffer.resize(MAX_DATAGRAM_SIZE); + auto written = quiche_retry(clientConnID.data(), clientConnID.size(), + serverConnID.data(), serverConnID.size(), + newServerConnID->data(), newServerConnID->size(), + token.data(), token.size(), + version, + buffer.data(), buffer.size()); + + if (written < 0) { + DEBUGLOG("failed to create retry packet " << written); + return; + } + + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + sock.sendTo(reinterpret_cast(buffer.data()), static_cast(written), peer); +} + +void handleVersionNegociation(Socket& sock, const PacketBuffer& clientConnID, const PacketBuffer& serverConnID, const ComboAddress& peer, PacketBuffer& buffer) +{ + buffer.resize(MAX_DATAGRAM_SIZE); + + auto written = quiche_negotiate_version(clientConnID.data(), clientConnID.size(), + serverConnID.data(), serverConnID.size(), + buffer.data(), buffer.size()); + + if (written < 0) { + DEBUGLOG("failed to create vneg packet " << written); + return; + } + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + sock.sendTo(reinterpret_cast(buffer.data()), static_cast(written), peer); +} + +void flushEgress(Socket& sock, QuicheConnection& conn, const ComboAddress& peer, PacketBuffer& buffer) +{ + buffer.resize(MAX_DATAGRAM_SIZE); + quiche_send_info send_info; + + while (true) { + auto written = quiche_conn_send(conn.get(), buffer.data(), buffer.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(buffer.data()), static_cast(written), peer); + } +} + +void configureQuiche(QuicheConfig& config, const QuicheParams& params, bool isHTTP) +{ + 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(), + // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast) + 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); + + if (isHTTP) { + /* see rfc9114 section 6.2. Unidirectional Streams: + Each endpoint needs to create at least one unidirectional stream for the HTTP control stream. + QPACK requires two additional unidirectional streams, and other extensions might require further streams. + Therefore, the transport parameters sent by both clients and servers MUST allow the peer to create at least three + unidirectional streams. + These transport parameters SHOULD also provide at least 1,024 bytes of flow-control credit to each unidirectional stream. + */ + quiche_config_set_initial_max_streams_uni(config.get(), 3U); + quiche_config_set_initial_max_stream_data_uni(config.get(), 1024U); + } + + // 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 -- cgit v1.2.3