#include "base64.hh"
#include "coverage.hh"
#include "doh.hh"
+#include "doq-common.hh"
#include "dolog.hh"
#include "sodcrypto.hh"
#include "threadname.hh"
setLuaSideEffect();
auto frontend = std::make_shared<DOH3Frontend>();
- 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);
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<int>(vars, "internalPipeBufferSize", frontend->d_internalPipeBufferSize);
- getOptionalValue<int>(vars, "idleTimeout", frontend->d_idleTimeout);
- getOptionalValue<std::string>(vars, "keyLogFile", frontend->d_keyLogFile);
+ getOptionalValue<int>(vars, "idleTimeout", frontend->d_quicheParams.d_idleTimeout);
+ getOptionalValue<std::string>(vars, "keyLogFile", frontend->d_quicheParams.d_keyLogFile);
{
std::string valueStr;
if (getOptionalValue<std::string>(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<bool>(vars, "ignoreTLSConfigurationErrors", ignoreTLSConfigurationErrors) > 0 && ignoreTLSConfigurationErrors) {
// and properly ignore the frontend before actually launching it
try {
std::map<int, std::string> 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());
setLuaSideEffect();
auto frontend = std::make_shared<DOQFrontend>();
- 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);
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<int>(vars, "internalPipeBufferSize", frontend->d_internalPipeBufferSize);
- getOptionalValue<int>(vars, "idleTimeout", frontend->d_idleTimeout);
- getOptionalValue<std::string>(vars, "keyLogFile", frontend->d_keyLogFile);
+ getOptionalValue<int>(vars, "idleTimeout", frontend->d_quicheParams.d_idleTimeout);
+ getOptionalValue<std::string>(vars, "keyLogFile", frontend->d_quicheParams.d_keyLogFile);
{
std::string valueStr;
if (getOptionalValue<std::string>(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<bool>(vars, "ignoreTLSConfigurationErrors", ignoreTLSConfigurationErrors) > 0 && ignoreTLSConfigurationErrors) {
// and properly ignore the frontend before actually launching it
try {
std::map<int, std::string> 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());
doh3.hh \
dolog.cc dolog.hh \
doq.hh \
+ doq-common.hh \
ednscookies.cc ednscookies.hh \
ednsextendederror.cc ednsextendederror.hh \
ednsoptions.cc ednsoptions.hh \
if HAVE_QUICHE
AM_CPPFLAGS += $(QUICHE_CFLAGS)
dnsdist_LDADD += $(QUICHE_LDFLAGS) $(QUICHE_LIBS)
+dnsdist_SOURCES += doq-common.cc
endif
if !HAVE_LUA_HPP
#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<const string, int> DOH3Frontend::s_available_cc_algorithms = {
- {"reno", QUICHE_CC_RENO},
- {"cubic", QUICHE_CC_CUBIC},
- {"bbr", QUICHE_CC_BBR},
-};
-
-using QuicheConnection = std::unique_ptr<quiche_conn, decltype(&quiche_conn_free)>;
-using QuicheHTTP3Connection = std::unique_ptr<quiche_h3_conn, decltype(&quiche_h3_conn_free)>;
-using QuicheConfig = std::unique_ptr<quiche_config, decltype(&quiche_config_free)>;
-using QuicheHTTP3Config = std::unique_ptr<quiche_h3_config, decltype(&quiche_h3_config_free)>;
+using namespace dnsdist::doq;
class H3Connection
{
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
{
}
}
-static void fillRandom(PacketBuffer& buffer, size_t size)
-{
- buffer.reserve(size);
- while (size > 0) {
- buffer.insert(buffer.end(), dnsdist::getRandomValue(std::numeric_limits<uint8_t>::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<enum quiche_cc_algorithm>(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);
d_server_config = std::make_unique<DOH3ServerConfig>(std::move(config), std::move(http3config), d_internalPipeBufferSize);
}
-static std::optional<PacketBuffer> 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<decltype(SodiumNonce::value)>{} /* 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<const uint8_t*>(&ttd), reinterpret_cast<const uint8_t*>(&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<const char*>(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<PacketBuffer> 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<const char*>(&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<const char*>(out.data()), written, peer);
-}
-
static std::optional<std::reference_wrapper<H3Connection>> getConnection(DOH3ServerConfig::ConnectionsMap& connMap, const PacketBuffer& connID)
{
auto iter = connMap.find(connID);
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));
return pair.first->second;
}
-static void flushEgress(Socket& sock, H3Connection& conn)
-{
- std::array<uint8_t, MAX_DATAGRAM_SIZE> 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<const char*>(out.data()), written, conn.d_peer);
- }
-}
-
std::unique_ptr<CrossProtocolQuery> getDOH3CrossProtocolQueryFromDQ(DNSQuestion& dnsQuestion, bool isResponse)
{
if (!dnsQuestion.ids.doh3u) {
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(),
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
#ifdef HAVE_DNS_OVER_HTTP3
+#include "doq-common.hh"
+
struct DOH3Frontend
{
DOH3Frontend();
void setup();
std::unique_ptr<DOH3ServerConfig> 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),
#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<const string, int> s_available_cc_algorithms;
};
struct DOH3Unit
--- /dev/null
+/*
+ * 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<const uint8_t*>(&ttd), reinterpret_cast<const uint8_t*>(&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<const char*>(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<uint8_t>::max()));
+ --size;
+ }
+}
+
+std::optional<PacketBuffer> 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<PacketBuffer> 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<const char*>(&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<const char*>(out.data()), written, peer);
+}
+
+void flushEgress(Socket& sock, QuicheConnection& conn, const ComboAddress& peer)
+{
+ std::array<uint8_t, MAX_DATAGRAM_SIZE> 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<const char*>(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<const uint8_t*>(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<enum quiche_cc_algorithm>(algo->second));
+ }
+
+ {
+ PacketBuffer resetToken;
+ fillRandom(resetToken, 16);
+ quiche_config_set_stateless_reset_token(config.get(), resetToken.data());
+ }
+}
+
+};
+
+#endif
--- /dev/null
+/*
+ * 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 <map>
+#include <memory>
+
+#include "config.h"
+
+#if defined(HAVE_DNS_OVER_QUIC) || defined(HAVE_DNS_OVER_HTTP3)
+
+#include <quiche.h>
+
+#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<const std::string, int> s_available_cc_algorithms = {
+ {"reno", QUICHE_CC_RENO},
+ {"cubic", QUICHE_CC_CUBIC},
+ {"bbr", QUICHE_CC_BBR},
+};
+
+using QuicheConnection = std::unique_ptr<quiche_conn, decltype(&quiche_conn_free)>;
+using QuicheHTTP3Connection = std::unique_ptr<quiche_h3_conn, decltype(&quiche_h3_conn_free)>;
+using QuicheConfig = std::unique_ptr<quiche_config, decltype(&quiche_config_free)>;
+using QuicheHTTP3Config = std::unique_ptr<quiche_h3_config, decltype(&quiche_h3_config_free)>;
+
+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<decltype(SodiumNonce::value)>{} /* 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<uint8_t, 4> DOQ_ALPN{'\x03', 'd', 'o', 'q'};
+static constexpr std::array<uint8_t, 3> DOH3_ALPN{'\x02', 'h', '3'};
+
+
+void fillRandom(PacketBuffer& buffer, size_t size);
+std::optional<PacketBuffer> getCID();
+PacketBuffer mintToken(const PacketBuffer& dcid, const ComboAddress& peer);
+std::optional<PacketBuffer> 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
#include "dnsdist-tcp.hh"
#include "dnsdist-random.hh"
-static std::string s_quicRetryTokenKey = newKey(false);
+#include "doq-common.hh"
-std::map<const string, int> DOQFrontend::s_available_cc_algorithms = {
- {"reno", QUICHE_CC_RENO},
- {"cubic", QUICHE_CC_CUBIC},
- {"bbr", QUICHE_CC_BBR},
-};
-
-using QuicheConnection = std::unique_ptr<quiche_conn, decltype(&quiche_conn_free)>;
-using QuicheConfig = std::unique_ptr<quiche_config, decltype(&quiche_config_free)>;
+using namespace dnsdist::doq;
class Connection
{
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:
}
}
-static void fillRandom(PacketBuffer& buffer, size_t size)
-{
- buffer.reserve(size);
- while (size > 0) {
- buffer.insert(buffer.end(), dnsdist::getRandomValue(std::numeric_limits<uint8_t>::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<uint8_t, 4> 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<enum quiche_cc_algorithm>(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<DOQServerConfig>(std::move(config), d_internalPipeBufferSize);
}
-static std::optional<PacketBuffer> 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<decltype(SodiumNonce::value)>{} /* 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<const uint8_t*>(&ttd), reinterpret_cast<const uint8_t*>(&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<const char*>(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<PacketBuffer> 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<const char*>(&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<const char*>(out.data()), written, peer);
-}
-
static std::optional<std::reference_wrapper<Connection>> getConnection(DOQServerConfig::ConnectionsMap& connMap, const PacketBuffer& connID)
{
auto iter = connMap.find(connID);
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));
return pair.first->second;
}
-static void flushEgress(Socket& sock, Connection& conn)
-{
- std::array<uint8_t, MAX_DATAGRAM_SIZE> 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<const char*>(out.data()), written, conn.d_peer);
- }
-}
-
std::unique_ptr<CrossProtocolQuery> getDOQCrossProtocolQueryFromDQ(DNSQuestion& dnsQuestion, bool isResponse)
{
if (!dnsQuestion.ids.doqu) {
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
#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
{
void setup();
std::unique_ptr<DOQServerConfig> 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),
#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<const string, int> s_available_cc_algorithms;
};
struct DOQUnit