}
if (ntohs(dnsHeader->arcount) == 0) {
- throw std::runtime_error("slowParseEDNSOptions() should not be called for queries that have no EDNS");
+ return false;
}
try {
// Line below detected an issue with the two ways of ordering PacketIDs (birthday and non-birthday)
assert(chain.first->key->domain == pident->domain); // NOLINT
// don't chain onto existing chained waiter or a chain already processed
- if (chain.first->key->fd > -1 && !chain.first->key->closed) {
+ if (chain.first->key->fd > -1 && !chain.first->key->closed && pident->ecsSubnet == chain.first->key->ecsSubnet) {
auto currentChainSize = chain.first->key->authReqChain.size();
*fileDesc = -static_cast<int>(currentChainSize + 1); // value <= -1, gets used in waitEvent / sendEvent later on
if (g_maxChainLength > 0 && currentChainSize >= g_maxChainLength) {
return LWResult::Result::Success;
}
+static bool checkIncomingECSSource(const PacketBuffer& packet, const Netmask& subnet);
+
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)
{
len = packet.size();
// In ecs hardening mode, we consider a missing or a mismatched ECS in the reply as a case for
- // retrying without ECS (matchingECSReceived only gets set if a matching ECS was received). The actual
- // logic to do that is in Syncres::doResolveAtThisIP()
- if (g_ECSHardening && pident->ecsSubnet && !*pident->matchingECSReceived) {
+ // retrying without ECS. The actual logic to do that is in Syncres::doResolveAtThisIP()
+ if (g_ECSHardening && pident->ecsSubnet && !checkIncomingECSSource(packet, *pident->ecsSubnet)) {
t_Counters.at(rec::Counter::ecsMissingCount)++;
return LWResult::Result::ECSMissing;
}
}
// resend event to everybody chained onto it
-static void doResends(MT_t::waiters_t::iterator& iter, const std::shared_ptr<PacketID>& resend, const PacketBuffer& content, const std::optional<bool>& matchingECSReceived)
+static void doResends(MT_t::waiters_t::iterator& iter, const std::shared_ptr<PacketID>& resend, const PacketBuffer& content)
{
// We close the chain for new entries, since they won't be processed anyway
iter->key->closed = true;
return;
}
- // Only set if g_ECSHardening
- if (matchingECSReceived) {
- iter->key->matchingECSReceived = matchingECSReceived;
- }
-
auto maxWeight = t_Counters.at(rec::Counter::maxChainWeight);
auto weight = iter->key->authReqChain.size() * content.size();
if (weight > maxWeight) {
PacketBuffer empty;
auto iter = g_multiTasker->getWaiters().find(pid);
if (iter != g_multiTasker->getWaiters().end()) {
- doResends(iter, pid, empty, false);
+ doResends(iter, pid, empty);
}
g_multiTasker->sendEvent(pid, &empty); // this denotes error (does retry lookup using other NS)
return;
if (!pident->domain.empty()) {
auto iter = g_multiTasker->getWaiters().find(pident);
if (iter != g_multiTasker->getWaiters().end()) {
- if (g_ECSHardening) {
- iter->key->matchingECSReceived = iter->key->ecsSubnet && checkIncomingECSSource(packet, *iter->key->ecsSubnet);
- }
- doResends(iter, pident, packet, iter->key->matchingECSReceived);
+ doResends(iter, pident, packet);
}
}
bool inIncompleteOkay{false};
uint16_t id{0}; // wait for a specific id/remote pair
uint16_t type{0}; // and this is its type
- std::optional<bool> matchingECSReceived; // only set in ecsHardened mode
TCPAction highState{TCPAction::DoingRead};
IOState lowState{IOState::NeedRead};
inline ostream& operator<<(ostream& ostr, const PacketID& pid)
{
- return ostr << "PacketID(id=" << pid.id << ",remote=" << pid.remote.toString() << ",type=" << pid.type << ",tcpsock=" << pid.tcpsock << ",fd=" << pid.fd << ',' << pid.domain << ')';
+ return ostr << "PacketID(id=" << pid.id << ",remote=" << pid.remote.toString() << ",type=" << pid.type << ",tcpsock=" << pid.tcpsock << ",fd=" << pid.fd << ",name=" << pid.domain << ",ecs=" << (pid.ecsSubnet ? pid.ecsSubnet->toString() : "") << ')';
}
inline ostream& operator<<(ostream& ostr, const shared_ptr<PacketID>& pid)
{
bool operator()(const std::shared_ptr<PacketID>& lhs, const std::shared_ptr<PacketID>& rhs) const
{
- if (std::tie(lhs->remote, lhs->tcpsock, lhs->type, lhs->ecsSubnet) < std::tie(rhs->remote, rhs->tcpsock, rhs->type, rhs->ecsSubnet)) {
+ if (std::tie(lhs->remote, lhs->tcpsock, lhs->type) < std::tie(rhs->remote, rhs->tcpsock, rhs->type)) {
return true;
}
- if (std::tie(lhs->remote, lhs->tcpsock, lhs->type, lhs->ecsSubnet) > std::tie(rhs->remote, rhs->tcpsock, rhs->type, rhs->ecsSubnet)) {
+ if (std::tie(lhs->remote, lhs->tcpsock, lhs->type) > std::tie(rhs->remote, rhs->tcpsock, rhs->type)) {
return false;
}
return lhs->domain < rhs->domain;
'zones': ['optout.example']},
'15': {'threads': 1,
'zones': ['insecure.optout.example', 'secure.optout.example', 'cname-secure.example']},
- '16': {'threads': 2,
+ '16': {'threads': 10,
'zones': ['delay1.example']},
- '17': {'threads': 2,
+ '17': {'threads': 10,
'zones': ['delay2.example']},
'18': {'threads': 1,
'zones': ['example']}
import os
import time
import requests
+import clientsubnetoption
from recursortests import RecursorTest
class ChainTest(RecursorTest):
'servfail-answers': 0,
'noerror-answers': (lambda x: x <= count),
})
+
+class ChainECSTest(RecursorTest):
+ """
+ These regression tests test the chaining of outgoing requests with ECS
+ """
+ _auth_zones = RecursorTest._default_auth_zones
+ _chainSize = 200
+ _confdir = 'ChainECS'
+ _wsPort = 8042
+ _wsTimeout = 2
+ _wsPassword = 'secretpassword'
+ _apiKey = 'secretapikey'
+
+ _config_template = """dnssec=validate
+ trace=no
+ edns-subnet-allow-list=0.0.0.0/0
+ use-incoming-edns-subnet=yes
+ edns-subnet-allow-list=0.0.0.0/0
+ devonly-regression-test-mode
+ webserver=yes
+ webserver-port=%d
+ webserver-address=127.0.0.1
+ webserver-password=%s
+ api-key=%s
+ max-concurrent-requests-per-tcp-connection=%s
+""" % (_wsPort, _wsPassword, _apiKey, _chainSize)
+
+ def testBasic(self):
+ """
+ Tests the case of #14624. Sending many equal requests could lead to ServFail because of
+ clashing waiter ids.
+ """
+ count = self._chainSize
+ name1 = '1.delay1.example.'
+ name2 = '2.delay1.example.'
+ exp1 = dns.rrset.from_text(name1, 0, dns.rdataclass.IN, 'TXT', 'a')
+ exp2 = dns.rrset.from_text(name2, 0, dns.rdataclass.IN, 'TXT', 'a')
+ queries = []
+ for i in range(count):
+ if i % 3 == 0:
+ name = name1
+ else:
+ name = name2
+ if i % 2 == 0:
+ ecso = clientsubnetoption.ClientSubnetOption('192.0.2.0', 24)
+ else:
+ ecso = clientsubnetoption.ClientSubnetOption('192.0.3.0', 24)
+ query = dns.message.make_query(name, 'TXT', use_edns=True, options=[ecso], want_dnssec=True)
+ query.flags |= dns.flags.AD
+ queries.append(query)
+
+ answers = self.sendTCPQueries(queries)
+ self.assertEqual(len(answers), count)
+
+ for i in range(count):
+ res = answers[i]
+ self.assertRcodeEqual(res, dns.rcode.NOERROR)
+ self.assertMessageIsAuthenticated(res)
+ if res.question[0].name.to_text() == name1:
+ self.assertRRsetInAnswer(res, exp1)
+ self.assertMatchingRRSIGInAnswer(res, exp1)
+ elif res.question[0].name.to_text() == name2:
+ self.assertRRsetInAnswer(res, exp2)
+ self.assertMatchingRRSIGInAnswer(res, exp2)
+ else:
+ print("?? " + res.question[0].name.to_text())
+ self.assertEqual(0, 1)
+
+ self.checkMetrics({
+ 'servfail-answers': 0,
+ 'noerror-answers': count,
+ })
+
+class ChainECSHardenedTest(RecursorTest):
+ """
+ These regression tests test the chaining of outgoing requests with ECS
+ """
+ _auth_zones = RecursorTest._default_auth_zones
+ _chainSize = 200
+ _confdir = 'ChainECSHardened'
+ _wsPort = 8042
+ _wsTimeout = 2
+ _wsPassword = 'secretpassword'
+ _apiKey = 'secretapikey'
+
+ _config_template = """dnssec=validate
+ trace=no
+ edns-subnet-allow-list=0.0.0.0/0
+ use-incoming-edns-subnet=yes
+ edns-subnet-allow-list=0.0.0.0/0
+ edns-subnet-harden=yes
+ devonly-regression-test-mode
+ webserver=yes
+ webserver-port=%d
+ webserver-address=127.0.0.1
+ webserver-password=%s
+ api-key=%s
+ max-concurrent-requests-per-tcp-connection=%s
+""" % (_wsPort, _wsPassword, _apiKey, _chainSize)
+
+ def testBasic(self):
+ """
+ Tests the case of #14624. Sending many equal requests could lead to ServFail because of
+ clashing waiter ids.
+ """
+ count = self._chainSize
+ name1 = '1.delay1.example.'
+ name2 = '2.delay1.example.'
+ exp1 = dns.rrset.from_text(name1, 0, dns.rdataclass.IN, 'TXT', 'a')
+ exp2 = dns.rrset.from_text(name2, 0, dns.rdataclass.IN, 'TXT', 'a')
+ queries = []
+ for i in range(count):
+ if i % 3 == 0:
+ name = name1
+ else:
+ name = name2
+ if i % 2 == 0:
+ ecso = clientsubnetoption.ClientSubnetOption('192.0.2.0', 24)
+ else:
+ ecso = clientsubnetoption.ClientSubnetOption('192.0.3.0', 24)
+ query = dns.message.make_query(name, 'TXT', use_edns=True, options=[ecso], want_dnssec=True)
+ query.flags |= dns.flags.AD
+ queries.append(query)
+
+ answers = self.sendTCPQueries(queries)
+ self.assertEqual(len(answers), count)
+
+ for i in range(count):
+ res = answers[i]
+ self.assertRcodeEqual(res, dns.rcode.NOERROR)
+ self.assertMessageIsAuthenticated(res)
+ if res.question[0].name.to_text() == name1:
+ self.assertRRsetInAnswer(res, exp1)
+ self.assertMatchingRRSIGInAnswer(res, exp1)
+ elif res.question[0].name.to_text() == name2:
+ self.assertRRsetInAnswer(res, exp2)
+ self.assertMatchingRRSIGInAnswer(res, exp2)
+ else:
+ print("?? " + res.question[0].name.to_text())
+ self.assertEqual(0, 1)
+
+ self.checkMetrics({
+ 'servfail-answers': 0,
+ 'noerror-answers': count,
+ })
+