static LockGuarded<CookieStore> s_cookiestore;
+std::string clearCookies()
+{
+ auto lock = s_cookiestore.lock();
+ lock->clear();
+ return "";
+}
+
void pruneCookies(time_t cutoff)
{
auto lock = s_cookiestore.lock();
}
else {
// Server responded with a wrong client cookie, fall back to TCP
+ cerr << "Wrong cookie" << endl;
lwr->d_validpacket = true;
- return LWResult::Result::BadCookie;
+ return LWResult::Result::Spoofed;
}
}
else {
// We sent a cookie out but forgot it?
+ cerr << "Cookie not found back"<< endl;
lwr->d_validpacket = true;
- return LWResult::Result::BadCookie;
+ return LWResult::Result::BadCookie; // XXX
}
}
+ else {
+ cerr << "Malformed cookie in reply"<< endl;
+ lwr->d_validpacket = true;
+ return LWResult::Result::BadCookie; // XXX
+ }
}
}
}
// Case: we sent out a cookie but did not get one back
if (cookieSentOut && !cookieFoundInReply && !*chained) {
+ cerr << "No cookie in reply"<< endl;
lwr->d_validpacket = true;
- return LWResult::Result::BadCookie;
+ return LWResult::Result::BadCookie; // XXX
}
if (outgoingLoggers) {
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);
+std::string clearCookies();
void pruneCookies(time_t cutoff);
void setAuthCookies(bool flag);
"add-nta DOMAIN [REASON] add a Negative Trust Anchor for DOMAIN with the comment REASON\n"
"add-ta DOMAIN DSRECORD add a Trust Anchor for DOMAIN with data DSRECORD\n"
"current-queries show currently active queries\n"
+ // "clear-cookies clear cookie table\n" XXX undocumented for now
"clear-dont-throttle-names [N...] remove names that are not allowed to be throttled. If N is '*', remove all\n"
"clear-dont-throttle-netmasks [N...]\n"
" remove netmasks that are not allowed to be throttled. If N is '*', remove all\n"
if (cmd == "dump-cache") {
return doDumpCache(socket, begin, end);
}
+ if (cmd == "clear-cookies") {
+ return {0, clearCookies()};
+ }
if (cmd == "dump-cookies") {
return doDumpToFile(socket, pleaseDumpCookiesMap, cmd, false);
}
}
auto match = d_eventTrace.add(RecEventTrace::AuthRequest, qname.toLogString() + '/' + qtype.toString(), true, 0);
updateQueryCounts(prefix, qname, remoteIP, doTCP, doDoT);
+ cerr << "doTCP " << doTCP << endl;
resolveret = asyncresolveWrapper(remoteIP, d_doDNSSEC, qname, auth, qtype.getCode(),
doTCP, sendRDQuery, &d_now, ednsmask, &lwr, &chained, nsName); // <- we go out on the wire!
d_eventTrace.add(RecEventTrace::AuthRequest, static_cast<int64_t>(lwr.d_rcode), false, match);
gotAnswer = doResolveAtThisIP(prefix, qname, qtype, lwr, ednsmask, auth, sendRDQuery, wasForwarded,
tns->first, *remoteIP, false, false, truncated, spoofed, context.extendedError);
}
+ cerr << "Got spoofed?!" << spoofed << endl;
if (forceTCP || (spoofed || (gotAnswer && truncated))) {
/* retry, over TCP this time */
gotAnswer = doResolveAtThisIP(prefix, qname, qtype, lwr, ednsmask, auth, sendRDQuery, wasForwarded,
except subprocess.CalledProcessError as e:
raise AssertionError('%s failed (%d): %s' % (rec_controlCmd, e.returncode, e.output))
+ @classmethod
+ def recControl(cls, confdir, *command):
+ rec_controlCmd = [os.environ['RECCONTROL'],
+ '--config-dir=%s' % confdir
+ ] + list(command)
+ try:
+ return subprocess.check_output(rec_controlCmd, text=True, stderr=subprocess.STDOUT)
+ except subprocess.CalledProcessError as e:
+ raise AssertionError('%s failed (%d): %s' % (rec_controlCmd, e.returncode, e.output))
+
@classmethod
def setUpSockets(cls):
print("Setting up UDP socket..")
requests>=2.1.0
Twisted>0.15.0
pyyaml==6.0.1
+siphash
--- /dev/null
+import dns
+import socket
+import os
+import time
+import threading
+from twisted.internet.protocol import Factory
+from twisted.internet.protocol import Protocol
+from twisted.internet.protocol import DatagramProtocol
+from twisted.internet import reactor
+
+from recursortests import RecursorTest
+
+class CookiesTest(RecursorTest):
+ _confdir = 'Cookies'
+
+ _config_template = """
+recursor:
+ forward_zones:
+ - zone: cookies.example
+ forwarders: [%s.25]
+outgoing:
+ cookies: true""" % (os.environ['PREFIX'])
+
+ _expectedCookies = 'no'
+ @classmethod
+ def generateRecursorConfig(cls, confdir):
+ super(CookiesTest, cls).generateRecursorYamlConfig(confdir)
+
+ @classmethod
+ def setUpClass(cls):
+ cls.setUpSockets()
+
+ cls.startResponders()
+
+ confdir = os.path.join('configs', cls._confdir)
+ cls.createConfigDir(confdir)
+
+ cls.generateRecursorConfig(confdir)
+ cls.startRecursor(confdir, cls._recursorPort)
+
+ print("Launching tests..")
+
+ @classmethod
+ def tearDownClass(cls):
+ cls.tearDownRecursor()
+
+ @classmethod
+ def startResponders(cls):
+ print("Launching responders..")
+
+ address = cls._PREFIX + '.25'
+ port = 53
+
+ reactor.listenUDP(port, UDPResponder(), interface=address)
+ reactor.listenTCP(port, TCPFactory(), interface=address)
+
+ if not reactor.running:
+ cls.Responder = threading.Thread(name='Responder', target=reactor.run, args=(False,))
+ cls.Responder.daemon = True
+ cls.Responder.start()
+ #cls._TCPResponder = threading.Thread(name='TCP Responder', target=reactor.run, args=(False,))
+ #cls._TCPResponder.daemon = True
+ #cls._TCPResponder.start()
+
+ def checkCookies(self, support):
+ confdir = os.path.join('configs', self._confdir)
+ output = self.recControl(confdir, 'dump-cookies', '-')
+ for line in output.splitlines():
+ tokens = line.split()
+ if tokens[0] != '127.0.0.25':
+ continue
+ print(tokens)
+ self.assertEqual(len(tokens), 5)
+ self.assertEqual(tokens[3], support)
+
+ def testAuthDoesnotSendCookies(self):
+ confdir = os.path.join('configs', self._confdir)
+ # Case: rec does not get a cookie back
+ expected = dns.rrset.from_text('a.cookies.example.', 15, dns.rdataclass.IN, 'A', '127.0.0.1')
+ query = dns.message.make_query('a.cookies.example.', 'A')
+ res = self.sendUDPQuery(query)
+ self.assertRcodeEqual(res, dns.rcode.NOERROR)
+ self.assertRRsetInAnswer(res, expected)
+ self.checkCookies('no')
+
+ def testAuthRepliesWithCookies(self):
+ confdir = os.path.join('configs', self._confdir)
+ # Case: rec gets a proper client and server cookie back
+ self.recControl(confdir, 'clear-cookies')
+ query = dns.message.make_query('b.cookies.example.', 'A')
+ expected = dns.rrset.from_text('b.cookies.example.', 15, dns.rdataclass.IN, 'A', '127.0.0.1')
+ res = self.sendUDPQuery(query)
+ self.assertRcodeEqual(res, dns.rcode.NOERROR)
+ self.assertRRsetInAnswer(res, expected)
+ self.checkCookies('yes')
+
+ # Case: we get a an correct client and server cookie back
+ # We do not clear the cookie tables, so the old server cookie gets re-used
+ query = dns.message.make_query('c.cookies.example.', 'A')
+ expected = dns.rrset.from_text('c.cookies.example.', 15, dns.rdataclass.IN, 'A', '127.0.0.1')
+ res = self.sendUDPQuery(query)
+ self.assertRcodeEqual(res, dns.rcode.NOERROR)
+ self.assertRRsetInAnswer(res, expected)
+ self.checkCookies('yes')
+
+ def testAuthSendsIncorrectClientCookie(self):
+ confdir = os.path.join('configs', self._confdir)
+ # Case: rec gets a an incorrect client cookie back
+ # Fails at the moment, as we do not do the right thing yet server side XXXX
+ self.recControl(confdir, 'clear-cookies')
+ query = dns.message.make_query('d.cookies.example.', 'A')
+ expected = dns.rrset.from_text('d.cookies.example.', 15, dns.rdataclass.IN, 'A', '127.0.0.1')
+ res = self.sendUDPQuery(query)
+ self.assertRcodeEqual(res, dns.rcode.NOERROR)
+ self.assertRRsetInAnswer(res, expected)
+ self.checkCookies('yes')
+
+ def testAuthSendsBADCOOKIEOverUDP(self):
+ confdir = os.path.join('configs', self._confdir)
+ # Case: rec gets a BADCOOKIE, even on retry and should fall back to TCP
+ self.recControl(confdir, 'clear-cookies')
+ query = dns.message.make_query('e.cookies.example.', 'A')
+ expected = dns.rrset.from_text('e.cookies.example.', 15, dns.rdataclass.IN, 'A', '127.0.0.1')
+ res = self.sendUDPQuery(query)
+ self.assertRcodeEqual(res, dns.rcode.NOERROR)
+ self.assertRRsetInAnswer(res, expected)
+ self.checkCookies('yes')
+
+
+class UDPResponder(DatagramProtocol):
+ def getCookie(self, message):
+ for option in message.options:
+ if option.otype == dns.edns.COOKIE: #and isinstance(option, dns.edns.CookieOption):
+ return option.data
+ return None
+
+ def createCookie(self, clientcookie):
+ clientcookie = clientcookie[0:8]
+ timestamp = int(time.time())
+ server = clientcookie + b'\x01\x00\x00\x00' + timestamp.to_bytes(4, 'big')
+ h = hash(server + b'\x01\x00\x00\x7f' + b'secret') % pow(2, 64)
+ full = dns.edns.GenericOption(dns.edns.COOKIE, server + h.to_bytes(8, 'big'))
+ return full
+
+ def question(self, datagram, tcp=False):
+ request = dns.message.from_wire(datagram)
+
+ response = dns.message.make_response(request)
+ response.flags = dns.flags.AA + dns.flags.QR
+
+ question = request.question[0]
+
+ # Case: do not send cookie back
+ if question.name == dns.name.from_text('a.cookies.example.') and question.rdtype == dns.rdatatype.A:
+ answer = dns.rrset.from_text('a.cookies.example.', 15, dns.rdataclass.IN, 'A', '127.0.0.1')
+ response.answer.append(answer)
+
+ # Case: do send cookie back
+ elif question.name == dns.name.from_text('b.cookies.example.') and question.rdtype == dns.rdatatype.A:
+ answer = dns.rrset.from_text('b.cookies.example.', 15, dns.rdataclass.IN, 'A', '127.0.0.1')
+ clientcookie = self.getCookie(request)
+ if clientcookie is not None:
+ response.use_edns(options = [self.createCookie(clientcookie)])
+ response.answer.append(answer)
+
+ # We get a good client and server cookie
+ elif question.name == dns.name.from_text('c.cookies.example.') and question.rdtype == dns.rdatatype.A:
+ answer = dns.rrset.from_text('c.cookies.example.', 15, dns.rdataclass.IN, 'A', '127.0.0.1')
+ clientcookie = self.getCookie(request)
+ if len(clientcookie) != 24:
+ raise AssertionError("expected full cookie, got len " + str(len(clientcookie)))
+ if clientcookie is not None:
+ response.use_edns(options = [self.createCookie(clientcookie)])
+ response.answer.append(answer)
+
+ # Case: do send incorrect client cookie back
+ elif question.name == dns.name.from_text('d.cookies.example.') and question.rdtype == dns.rdatatype.A:
+ answer = dns.rrset.from_text('d.cookies.example.', 15, dns.rdataclass.IN, 'A', '127.0.0.1')
+ clientcookie = self.getCookie(request)
+ if clientcookie is not None:
+ mod = bytearray(clientcookie)
+ mod[0] = 1
+ response.use_edns(options = [self.createCookie(bytes(mod))])
+ response.answer.append(answer)
+
+ # Case: do send BADCOOKIE cookie back if UDP
+ elif question.name == dns.name.from_text('e.cookies.example.') and question.rdtype == dns.rdatatype.A:
+ answer = dns.rrset.from_text('e.cookies.example.', 15, dns.rdataclass.IN, 'A', '127.0.0.1')
+ clientcookie = self.getCookie(request)
+ if clientcookie is not None:
+ response.use_edns(options = [self.createCookie(clientcookie)])
+ if not tcp:
+ response.set_rcode(23) # BADCOOKIE
+ response.answer.append(answer)
+
+ return response.to_wire()
+
+ def datagramReceived(self, datagram, address):
+ response = self.question(datagram)
+ self.transport.write(response, address)
+
+class TCPResponder(Protocol):
+ def dataReceived(self, data):
+ handler = UDPResponder()
+ response = handler.question(data[2:], True)
+ length = len(response)
+ header = length.to_bytes(2, 'big')
+ self.transport.write(header + response)
+
+class TCPFactory(Factory):
+ def buildProtocol(self, addr):
+ return TCPResponder()
--- /dev/null
+import dns
+import os
+from recursortests import RecursorTest
+
+class SimpleCookiesTest(RecursorTest):
+ _confdir = 'SimpleCookies'
+
+ _config_template = """
+recursor:
+ auth_zones:
+ - zone: authzone.example
+ file: configs/%s/authzone.zone
+dnssec:
+ validation: validate
+outgoing:
+ cookies: true""" % _confdir
+
+ _expectedCookies = 'no'
+ @classmethod
+ def generateRecursorConfig(cls, confdir):
+ authzonepath = os.path.join(confdir, 'authzone.zone')
+ with open(authzonepath, 'w') as authzone:
+ authzone.write("""$ORIGIN authzone.example.
+@ 3600 IN SOA {soa}
+@ 3600 IN A 192.0.2.88
+""".format(soa=cls._SOA))
+ super(SimpleCookiesTest, cls).generateRecursorYamlConfig(confdir)
+
+ def checkCookies(self):
+ confdir = os.path.join('configs', self._confdir)
+ output = self.recControl(confdir, 'dump-cookies', '-')
+ for line in output.splitlines():
+ tokens = line.split()
+ if tokens[0] == ';' or tokens[0] == 'dump-cookies:':
+ continue
+ print(tokens)
+ self.assertEqual(len(tokens), 5)
+ self.assertEqual(tokens[3], self._expectedCookies)
+
+ def testSOAs(self):
+ for zone in ['.', 'example.', 'secure.example.']:
+ expected = dns.rrset.from_text(zone, 0, dns.rdataclass.IN, 'SOA', self._SOA)
+ query = dns.message.make_query(zone, 'SOA', want_dnssec=True)
+ query.flags |= dns.flags.AD
+
+ res = self.sendUDPQuery(query)
+
+ self.assertMessageIsAuthenticated(res)
+ self.assertRRsetInAnswer(res, expected)
+ self.assertMatchingRRSIGInAnswer(res, expected)
+ self.checkCookies()
+
+ def testA(self):
+ expected = dns.rrset.from_text('ns.secure.example.', 0, dns.rdataclass.IN, 'A', '{prefix}.9'.format(prefix=self._PREFIX))
+ query = dns.message.make_query('ns.secure.example', 'A', want_dnssec=True)
+ query.flags |= dns.flags.AD
+
+ res = self.sendUDPQuery(query)
+
+ self.assertMessageIsAuthenticated(res)
+ self.assertRRsetInAnswer(res, expected)
+ self.assertMatchingRRSIGInAnswer(res, expected)
+ self.checkCookies()
+
+ def testDelegation(self):
+ query = dns.message.make_query('example', 'NS', want_dnssec=True)
+ query.flags |= dns.flags.AD
+
+ expectedNS = dns.rrset.from_text('example.', 0, 'IN', 'NS', 'ns1.example.', 'ns2.example.')
+
+ res = self.sendUDPQuery(query)
+
+ self.assertMessageIsAuthenticated(res)
+ self.assertRRsetInAnswer(res, expectedNS)
+ self.checkCookies()
+
+ def testBogus(self):
+ query = dns.message.make_query('ted.bogus.example', 'A', want_dnssec=True)
+
+ res = self.sendUDPQuery(query)
+
+ self.assertRcodeEqual(res, dns.rcode.SERVFAIL)
+ self.checkCookies()
+
+ def testAuthZone(self):
+ query = dns.message.make_query('authzone.example', 'A', want_dnssec=True)
+
+ expectedA = dns.rrset.from_text('authzone.example.', 0, 'IN', 'A', '192.0.2.88')
+
+ res = self.sendUDPQuery(query)
+
+ self.assertRcodeEqual(res, dns.rcode.NOERROR)
+ self.assertRRsetInAnswer(res, expectedA)
+ self.checkCookies()
+
+ def testLocalhost(self):
+ queryA = dns.message.make_query('localhost', 'A', want_dnssec=True)
+ expectedA = dns.rrset.from_text('localhost.', 0, 'IN', 'A', '127.0.0.1')
+
+ queryPTR = dns.message.make_query('1.0.0.127.in-addr.arpa', 'PTR', want_dnssec=True)
+ expectedPTR = dns.rrset.from_text('1.0.0.127.in-addr.arpa.', 0, 'IN', 'PTR', 'localhost.')
+
+ resA = self.sendUDPQuery(queryA)
+ resPTR = self.sendUDPQuery(queryPTR)
+
+ self.assertRcodeEqual(resA, dns.rcode.NOERROR)
+ self.assertRRsetInAnswer(resA, expectedA)
+
+ self.assertRcodeEqual(resPTR, dns.rcode.NOERROR)
+ self.assertRRsetInAnswer(resPTR, expectedPTR)
+ self.checkCookies()
+
+ def testLocalhostSubdomain(self):
+ queryA = dns.message.make_query('foo.localhost', 'A', want_dnssec=True)
+ expectedA = dns.rrset.from_text('foo.localhost.', 0, 'IN', 'A', '127.0.0.1')
+
+ resA = self.sendUDPQuery(queryA)
+
+ self.assertRcodeEqual(resA, dns.rcode.NOERROR)
+ self.assertRRsetInAnswer(resA, expectedA)
+ self.checkCookies()
+
+ def testIslandOfSecurity(self):
+ query = dns.message.make_query('cname-to-islandofsecurity.secure.example.', 'A', want_dnssec=True)
+
+ expectedCNAME = dns.rrset.from_text('cname-to-islandofsecurity.secure.example.', 0, 'IN', 'CNAME', 'node1.islandofsecurity.example.')
+ expectedA = dns.rrset.from_text('node1.islandofsecurity.example.', 0, 'IN', 'A', '192.0.2.20')
+
+ res = self.sendUDPQuery(query)
+
+ self.assertRcodeEqual(res, dns.rcode.NOERROR)
+ self.assertRRsetInAnswer(res, expectedA)
+ self.checkCookies()
+
+class SimpleCookiesAuthEnabledTest(SimpleCookiesTest):
+ _confdir = 'SimpleCookiesAuthEnabled'
+ _expectedCookies = 'yes'
+
+ @classmethod
+ def generateAuthConfig(cls, confdir, threads):
+ super(SimpleCookiesAuthEnabledTest, cls).generateAuthConfig(confdir, threads, "edns-cookie-secret=01234567890123456789012345678901")
+