return ret;
}
+string EDNSCookiesOpt::toDisplayString() const
+{
+ std::string ret = makeHexDump(client, "");;
+ if (!server.empty()) {
+ ret += '|';
+ if (server.length() != 16) {
+ // It isn't a rfc9018 one
+ ret += makeHexDump(server, "");
+ }
+ else {
+ // It very likely is a rfc9018 one
+ ret += makeHexDump(server.substr(0, 1), ""); // Version
+ ret += '|';
+ ret += makeHexDump(server.substr(1, 3), ""); // Reserved
+ ret += '|';
+ ret += makeHexDump(server.substr(4, 4), ""); // Timestamp
+ ret += '|';
+ ret += makeHexDump(server.substr(8, 8), ""); // Hash
+ }
+ }
+ return ret;
+}
+
void EDNSCookiesOpt::getEDNSCookiesOptFromString(const char* option, unsigned int len)
{
client.clear();
[[nodiscard]] bool isValid(const std::string& secret, const ComboAddress& source) const;
void makeClientCookie();
bool makeServerCookie(const std::string& secret, const ComboAddress& source);
+
[[nodiscard]] std::string makeOptString() const;
+ [[nodiscard]] std::string toDisplayString() const;
[[nodiscard]] std::string getServer() const
{
return server;
ratelimitedlog.hh \
rcpgenerator.cc rcpgenerator.hh \
rec-carbon.cc \
+ rec-cookiestore.cc rec-cookiestore.hh \
rec-eventtrace.cc rec-eventtrace.hh \
rec-lua-conf.hh rec-lua-conf.cc \
rec-main.hh rec-main.cc \
#include "rec-protozero.hh"
#include "uuid-utils.hh"
#include "rec-tcpout.hh"
+#include "rec-cookiestore.hh"
+
+static bool g_cookies = true;
thread_local TCPOutConnectionManager t_tcp_manager;
std::shared_ptr<Logr::Logger> g_slogout;
bool g_paddingOutgoing;
bool g_ECSHardening;
+static LockGuarded<CookieStore> s_cookiestore;
+
+void pruneCookies(time_t cutoff)
+{
+ auto lock = s_cookiestore.lock();
+ lock->prune(cutoff);
+}
+
+uint64_t dumpCookies(int fileDesc)
+{
+ CookieStore copy;
+ {
+ auto lock = s_cookiestore.lock();
+ copy = *lock;
+ }
+ return CookieStore::dump(copy, fileDesc);
+}
+
void remoteLoggerQueueData(RemoteLoggerInterface& rli, const std::string& data)
{
auto ret = rli.queueData(data);
*/
pw.getHeader()->cd = (sendRDQuery && g_dnssecmode != DNSSECMode::Off);
- string ping;
std::optional<EDNSSubnetOpts> subnetOpts = std::nullopt;
+ std::optional<ComboAddress> addressToBindTo;
+ std::optional<EDNSCookiesOpt> cookieSentOut;
+
if (EDNS0Level > 0) {
DNSPacketWriter::optvect_t opts;
if (srcmask) {
opts.emplace_back(EDNSOptionCode::ECS, subnetOpts->makeOptString());
}
+ if (g_cookies) {
+ auto lock = s_cookiestore.lock();
+ auto found = lock->find(address);
+ if (found != lock->end()) {
+ if (found->d_support) {
+ cookieSentOut = found->d_cookie;
+ addressToBindTo = found->d_localaddress;
+ opts.emplace_back(EDNSOptionCode::COOKIE, cookieSentOut->makeOptString());
+ found->d_lastupdate = now->tv_sec;
+ cerr << "Sending stored cookie info to " << address.toString() << ": " << found->d_cookie.toDisplayString() << endl;
+ }
+ else {
+ cerr << "This server does not support cookies" << endl;
+ }
+ }
+ else {
+ CookieEntry entry;
+ entry.d_address = address;
+ entry.d_cookie.makeClientCookie();
+ cookieSentOut = entry.d_cookie;
+ entry.d_lastupdate = now->tv_sec;
+ entry.d_support = false;
+ lock->emplace(entry);
+ opts.emplace_back(EDNSOptionCode::COOKIE, cookieSentOut->makeOptString());
+ cerr << "We're sending new client cookie info from to " << address.toString() << ": " << entry.d_cookie.toDisplayString() << endl;
+ }
+ }
+
if (dnsOverTLS && g_paddingOutgoing) {
addPadding(pw, bufsize, opts);
}
srcmask = boost::none; // this is also our return value, even if EDNS0Level == 0
- // We only store the localip if needed for fstrm logging
+ // We only store the localip if needed for fstrm logging or cookie support
ComboAddress localip;
-#ifdef HAVE_FSTRM
bool fstrmQEnabled = false;
bool fstrmREnabled = false;
+#ifdef HAVE_FSTRM
if (isEnabledForQueries(fstrmLoggers)) {
fstrmQEnabled = true;
}
if (!doTCP) {
int queryfd;
-
- ret = asendto(vpacket.data(), vpacket.size(), 0, address, qid, domain, type, subnetOpts, &queryfd, *now);
-
+ try {
+ ret = asendto(vpacket.data(), vpacket.size(), 0, address, addressToBindTo, qid, domain, type, subnetOpts, &queryfd, *now);
+ }
+ catch (const PDNSException& e) {
+ if (addressToBindTo) {
+ // Cookie info already has been added to packet, so we must retry from a higher level
+ auto lock = s_cookiestore.lock();
+ lock->erase(address);
+ return LWResult::Result::BindError;
+ }
+ throw;
+ }
if (ret != LWResult::Result::Success) {
return ret;
}
*chained = true;
}
-#ifdef HAVE_FSTRM
if (!*chained) {
- if (fstrmQEnabled || fstrmREnabled) {
+ if (cookieSentOut || fstrmQEnabled || fstrmREnabled) {
localip.sin4.sin_family = address.sin4.sin_family;
socklen_t slen = address.getSocklen();
(void)getsockname(queryfd, reinterpret_cast<sockaddr*>(&localip), &slen); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast))
logFstreamQuery(fstrmLoggers, queryTime, localip, address, DnstapMessage::ProtocolType::DoUDP, context.d_auth ? context.d_auth : boost::none, vpacket);
}
}
-#endif /* HAVE_FSTRM */
// sleep until we see an answer to this, interface to mtasker
ret = arecvfrom(buf, 0, address, len, qid, domain, type, queryfd, subnetOpts, *now);
// peer has closed it on error, so we retry. At some point we
// *will* get a new connection, so this loop is not endless.
isNew = true; // tcpconnect() might throw for new connections. In that case, we want to break the loop, scanbuild complains here, which is a false positive afaik
+ // XXX cookie case: bind to local address
isNew = tcpconnect(address, connection, dnsOverTLS, nsName);
ret = tcpsendrecv(address, connection, localip, vpacket, len, buf);
#ifdef HAVE_FSTRM
lwr->d_records.push_back(answer);
}
+ bool cookieFoundInReply = false;
if (EDNSOpts edo; EDNS0Level > 0 && getEDNSOpts(mdp, &edo)) {
lwr->d_haveEDNS = true;
}
}
}
+ if (g_cookies && !*chained) {
+ for (const auto& opt : edo.d_options) {
+ if (opt.first == EDNSOptionCode::COOKIE) {
+ EDNSCookiesOpt received;
+ if (received.makeFromString(opt.second)) {
+ cookieFoundInReply = true;
+ cerr << "Received cookie info back from " << address.toString() << ": " << received.toDisplayString() << endl;
+ auto lock = s_cookiestore.lock();
+ auto found = lock->find(address);
+ if (found != lock->end()) {
+ if (received.getClient() == cookieSentOut->getClient()) {
+ cerr << "Client cookie matched! Storing with localAddress " << localip.toString() << endl;
+ found->d_localaddress = localip;
+ found->d_cookie = received;
+ found->d_lastupdate = now->tv_sec;
+ found->d_support = true;
+ uint16_t ercode = (edo.d_extRCode << 4) | lwr->d_rcode;
+ if (ercode == ERCode::BADCOOKIE) {
+ lwr->d_validpacket = true;
+ return LWResult::Result::BadCookie;
+ }
+ }
+ else {
+ // Server responded with a wrong client cookie, fall back to TCP
+ lwr->d_validpacket = true;
+ return LWResult::Result::BadCookie;
+ }
+ }
+ else {
+ // We sent a cookie out but forgot it?
+ lwr->d_validpacket = true;
+ return LWResult::Result::BadCookie;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Case: we sent out a cookie but did not get one back
+ if (cookieSentOut && !cookieFoundInReply && !*chained) {
+ lwr->d_validpacket = true;
+ return LWResult::Result::BadCookie;
}
if (outgoingLoggers) {
Spoofed = 4, /* Spoofing attempt (too many near-misses) */
ChainLimitError = 5,
ECSMissing = 6,
+ BadCookie = 7,
+ BindError = 8,
};
[[nodiscard]] static bool isLimitError(Result res)
class EDNSSubnetOpts;
-LWResult::Result asendto(const void* data, size_t len, int flags, const ComboAddress& toAddress, uint16_t qid,
+LWResult::Result asendto(const void* data, size_t len, int flags, const ComboAddress& toAddress,
+ std::optional<ComboAddress>& localAddress, uint16_t qid,
const DNSName& domain, uint16_t qtype, const std::optional<EDNSSubnetOpts>& ecs, int* fileDesc, timeval& now);
LWResult::Result arecvfrom(PacketBuffer& packet, int flags, const ComboAddress& fromAddr, size_t& len, uint16_t qid,
const DNSName& domain, uint16_t qtype, int fileDesc, const std::optional<EDNSSubnetOpts>& ecs, const struct timeval& now);
LWResult::Result asyncresolve(const ComboAddress& address, const DNSName& domain, int type, bool doTCP, bool sendRDQuery, int EDNS0Level, struct timeval* now, boost::optional<Netmask>& srcmask, const ResolveContext& context, const std::shared_ptr<std::vector<std::unique_ptr<RemoteLogger>>>& outgoingLoggers, const std::shared_ptr<std::vector<std::unique_ptr<FrameStreamLogger>>>& fstrmLoggers, const std::set<uint16_t>& exportTypes, LWResult* lwr, bool* chained);
+uint64_t dumpCookies(int fileDesc);
+void pruneCookies(time_t cutoff);
GlobalStateHolder<SuffixMatchNode> g_DoTToAuthNames;
uint64_t g_latencyStatSize;
-LWResult::Result UDPClientSocks::getSocket(const ComboAddress& toaddr, int* fileDesc)
+LWResult::Result UDPClientSocks::getSocket(const ComboAddress& toaddr, const std::optional<ComboAddress>& localAddress, int* fileDesc)
{
- *fileDesc = makeClientSocket(toaddr.sin4.sin_family);
+ *fileDesc = makeClientSocket(toaddr.sin4.sin_family, localAddress);
if (*fileDesc < 0) { // temporary error - receive exception otherwise
return LWResult::Result::OSLimitError;
}
}
// returns -1 for errors which might go away, throws for ones that won't
-int UDPClientSocks::makeClientSocket(int family)
+int UDPClientSocks::makeClientSocket(int family, const std::optional<ComboAddress>& localAddress)
{
int ret = socket(family, SOCK_DGRAM, 0); // turns out that setting CLO_EXEC and NONBLOCK from here is not a performance win on Linux (oddly enough)
} while (g_avoidUdpSourcePorts.count(port) != 0);
}
- sin = pdns::getQueryLocalAddress(family, port); // does htons for us
+ if (localAddress) {
+ cerr << "Binding to local address associated with cookie: " << localAddress->toString() << endl;
+ sin = *localAddress;
+ sin.setPort(port);
+ }
+ else {
+ sin = pdns::getQueryLocalAddress(family, port); // does htons for us
+ cerr << "Bound to random local address " << sin.toString() << endl;
+ }
if (::bind(ret, reinterpret_cast<struct sockaddr*>(&sin), sin.getSocklen()) >= 0) { // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast)
break;
}
/* these two functions are used by LWRes */
LWResult::Result asendto(const void* data, size_t len, int /* flags */,
- const ComboAddress& toAddress, uint16_t qid, const DNSName& domain, uint16_t qtype, const std::optional<EDNSSubnetOpts>& ecs, int* fileDesc, timeval& now)
+ const ComboAddress& toAddress, std::optional<ComboAddress>& localAddress, uint16_t qid, const DNSName& domain, uint16_t qtype, const std::optional<EDNSSubnetOpts>& ecs, int* fileDesc, timeval& now)
{
auto pident = std::make_shared<PacketID>();
}
}
- auto ret = t_udpclientsocks->getSocket(toAddress, fileDesc);
+ auto ret = t_udpclientsocks->getSocket(toAddress, localAddress, fileDesc);
if (ret != LWResult::Result::Success) {
return ret;
}
--- /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 "misc.hh"
+#include "rec-cookiestore.hh"
+
+using timebuf_t = std::array<char, 64>;
+
+extern const char* timestamp(time_t arg, timebuf_t& buf); // XXX
+
+void CookieStore::prune(time_t cutoff)
+{
+ auto& ind = get<time_t>();
+ ind.erase(ind.begin(), ind.upper_bound(cutoff));
+}
+
+uint64_t CookieStore::dump(const CookieStore& copy, int fileDesc)
+{
+ int newfd = dup(fileDesc);
+ if (newfd == -1) {
+ return 0;
+ }
+ auto filePtr = pdns::UniqueFilePtr(fdopen(newfd, "w"));
+ if (!filePtr) {
+ close(newfd);
+ return 0;
+ }
+ uint64_t count = 0;
+
+ fprintf(filePtr.get(), "; cookie dump follows\n; server\tlocal\tcookie\tsupport\tts\n");
+ for (const auto& entry : copy) {
+ count++;
+ timebuf_t tmp;
+ fprintf(filePtr.get(), "%s\t%s\t%s\t%s\t%s\n",
+ entry.d_address.toString().c_str(), entry.d_localaddress.toString().c_str(),
+ entry.d_cookie.toDisplayString().c_str(),
+ entry.d_support ? "yes" : "no",
+ timestamp(entry.d_lastupdate, tmp));
+ }
+ return count;
+}
--- /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
+
+/*
+ CookieStore is used to keep track of client cookies used for contacting authoritative servers.
+ According to RFC 7873 and RFC 9018, it has the following design.
+
+ - Cookies are stored with an auth IP address as primary index and are generated randomly.
+
+ - If the the does not support cookies, this is marked as such and no cookies will be sent to it
+ for a period of time. When a cookie is sent again, it must be a newly generated one.
+
+ - A cookie is stored together with the client IP (as rec can have many). If a server is to be
+ contacted again, it should use the same bound IP.
+
+ - Although it is perfectly fine for a client cookie to live for a long time, this design will
+ flush entries older that a certain period of time, to avoid an ever growing CookieStore.
+
+*/
+
+#include <boost/utility.hpp>
+#include <boost/multi_index_container.hpp>
+#include <boost/multi_index/ordered_index.hpp>
+#include <boost/multi_index/hashed_index.hpp>
+#include <boost/multi_index/key_extractors.hpp>
+#include <boost/multi_index/sequenced_index.hpp>
+
+#include "iputils.hh"
+#include "ednscookies.hh"
+
+using namespace ::boost::multi_index;
+
+struct CookieEntry
+{
+ ComboAddress d_address;
+ mutable ComboAddress d_localaddress; // The address we were bound to, see RFC 9018
+ mutable EDNSCookiesOpt d_cookie; // Contains both client and server cookie
+ mutable time_t d_lastupdate{};
+ mutable bool d_support;
+};
+
+class CookieStore : public multi_index_container < CookieEntry,
+ indexed_by < ordered_unique<tag<ComboAddress>, member<CookieEntry, ComboAddress, &CookieEntry::d_address>>,
+ ordered_non_unique<tag<time_t>, member<CookieEntry, time_t, &CookieEntry::d_lastupdate>>>>
+{
+public:
+ void prune(time_t cutoff);
+ static uint64_t dump(const CookieStore&, int fileDesc);
+};
SyncRes::pruneSaveParentsNSSets(now.tv_sec);
});
+ static PeriodicTask pruneCookiesTask{"pruneCookiesTask", 30};
+ pruneCookiesTask.runIfDue(now, [now]() {
+ pruneCookies(now.tv_sec - 1800);
+ });
+
// By default, refresh at 80% of max-cache-ttl with a minimum period of 10s
const unsigned int minRootRefreshInterval = 10;
static PeriodicTask rootUpdateTask{"rootUpdateTask", std::max(SyncRes::s_maxcachettl * 8 / 10, minRootRefreshInterval)};
{
}
- LWResult::Result getSocket(const ComboAddress& toaddr, int* fileDesc);
+ LWResult::Result getSocket(const ComboAddress& toaddr, const std::optional<ComboAddress>& localAddress, int* fileDesc);
// return a socket to the pool, or simply erase it
void returnSocket(int fileDesc);
private:
// returns -1 for errors which might go away, throws for ones that won't
- static int makeClientSocket(int family);
+ static int makeClientSocket(int family, const std::optional<ComboAddress>& localAddress);
};
enum class PaddingMode
}
// NOLINTBEGIN(cppcoreguidelines-owning-memory)
+static uint64_t* pleaseDumpCookiesMap(int fileDesc)
+{
+ return new uint64_t(dumpCookies(fileDesc));
+}
+
static uint64_t* pleaseDumpEDNSMap(int fileDesc)
{
return new uint64_t(SyncRes::doEDNSDump(fileDesc));
"clear-nta [DOMAIN]... Clear the Negative Trust Anchor for DOMAINs, if no DOMAIN is specified, remove all\n"
"clear-ta [DOMAIN]... Clear the Trust Anchor for DOMAINs\n"
"dump-cache <filename> [type...] dump cache contents to the named file, type is r, n, p or a\n"
+ "dump-cookies <filename> dump the contents of the cookie data to the namewd file\n"
"dump-dot-probe-map <filename> dump the contents of the DoT probe map to the named file\n"
"dump-edns [status] <filename> dump EDNS status to the named file\n"
"dump-failedservers <filename> dump the failed servers to the named file\n"
if (cmd == "dump-cache") {
return doDumpCache(socket, begin, end);
}
+ if (cmd == "dump-cookies") {
+ return doDumpToFile(socket, pleaseDumpCookiesMap, cmd, false);
+ }
if (cmd == "dump-dot-probe-map") {
return doDumpToFile(socket, pleaseDumpDoTProbeMap, cmd, false);
}
const set<string> fileCommands = {
"dump-cache",
+ "dump-cookies",
"dump-edns",
"dump-ednsstatus",
"dump-nsspeeds",
return buf.data();
}
-static const char* timestamp(time_t arg, timebuf_t& buf)
+const char* timestamp(time_t arg, timebuf_t& buf)
{
const std::string s_timestampFormat = "%Y-%m-%dT%T";
struct tm tmval{};
auto lock = s_ednsstatus.lock(); // all three branches below need a lock
// Determine new mode
+ if (ret == LWResult::Result::BindError) {
+ cerr << "BindError, retrying with new client cookie and no specific address to bind to" << endl;
+ // BindError is only generated when cookies are active and we failed to bind to a local
+ // address associated with a cookie, see RFC9018 section 3 last paragraph. We assume the
+ // called code alread erased the cookie info.
+ // This is the first path that re-iterates the loop
+ continue;
+ }
+ else if (res->d_validpacket && res->d_haveEDNS && ret == LWResult::Result::BadCookie) {
+ cerr << "Retrying with received server cookie" << endl;
+ // We assume the received cookie was stored and will be used in the second iteration
+ // This is the second path that re-iterates the loop
+ continue;
+ }
if (res->d_validpacket && !res->d_haveEDNS && res->d_rcode == RCode::FormErr) {
mode = EDNSStatus::NOEDNS;
auto ednsstatus = lock->insert(address).first;
auto& ind = lock->get<ComboAddress>();
lock->setMode(ind, ednsstatus, mode, d_now.tv_sec);
- // This is the only path that re-iterates the loop
+ // This is the third path that re-iterates the loop
continue;
}
if (!res->d_haveEDNS) {
}
}
+ cerr << "asyncrW: returns " << int(resolveret) << " rcode is " << int(lwr.d_rcode) << endl;
+
/* preoutquery killed the query by setting dq.rcode to -3 */
if (preOutQueryRet == -3) {
throw ImmediateServFailException("Query killed by policy");
d_totUsec += lwr.d_usec;
- if (resolveret == LWResult::Result::Spoofed) {
+ if (resolveret == LWResult::Result::Spoofed || resolveret == LWResult::Result::BadCookie) {
+ cerr << "Acting as we got a spoof" << endl;
spoofed = true;
return false;
}
}
if (forceTCP || (spoofed || (gotAnswer && truncated))) {
/* retry, over TCP this time */
+ cerr << "Retry over TCP" << endl;
gotAnswer = doResolveAtThisIP(prefix, qname, qtype, lwr, ednsmask, auth, sendRDQuery, wasForwarded,
tns->first, *remoteIP, true, doDoT, truncated, spoofed, context.extendedError);
}