From: Remi Gacogne Date: Fri, 3 Jun 2016 09:54:58 +0000 (+0200) Subject: dnsdist: Make dnsdist {A,I}XFR aware, document possible issues X-Git-Tag: auth-4.0.0-rc1~47^2 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=refs%2Fpull%2F3975%2Fhead;p=thirdparty%2Fpdns.git dnsdist: Make dnsdist {A,I}XFR aware, document possible issues --- diff --git a/pdns/README-dnsdist.md b/pdns/README-dnsdist.md index 83cc56d17b..b0d03f8513 100644 --- a/pdns/README-dnsdist.md +++ b/pdns/README-dnsdist.md @@ -987,6 +987,45 @@ If you forgot to write down the provider fingerprint value after generating the Provider fingerprint is: E1D7:2108:9A59:BF8D:F101:16FA:ED5E:EA6A:9F6C:C78F:7F91:AF6B:027E:62F4:69C3:B1AA ``` +AXFR, IXFR and NOTIFY +--------------------- +When `dnsdist` is deployed in front of a master authoritative server, it might +receive AXFR or IXFR queries destined to this master. There are two issues +that can arise in this kind of setup: + + * If the master is part of a pool of servers, the first SOA query can be directed + by `dnsdist` to a different server than the following AXFR/IXFR one. If all servers are not + perfectly synchronised at all times, it might to synchronisation issues. + * If the master only allows AXFR/IXFR based on the source address of the requestor, + it might be confused by the fact that the source address will be the one from + the `dnsdist` server. + +The first issue can be solved by routing SOA, AXFR and IXFR requests explicitely +to the master: + +``` +> newServer({address="192.168.1.2", name="master", pool={"master", "otherpool"}}) +> addAction(OrRule({QTypeRule(dnsdist.SOA), QTypeRule(dnsdist.AXFR), QTypeRule(dnsdist.IXFR)}), PoolAction("master")) +``` + +The second one might requires allowing AXFR/IXFR from the `dnsdist` source address +and moving the source address check on `dnsdist`'s side: + +``` +> addAction(AndRule({OrRule({QTypeRule(dnsdist.AXFR), QTypeRule(dnsdist.IXFR)}), NotRule(makeRule("192.168.1.0/24"))}), RCodeAction(dnsdist.REFUSED)) +``` + +When `dnsdist` is deployed in front of slaves, however, an issue might arise with NOTIFY +queries, because the slave will receive a notification coming from the `dnsdist` address, +and not the master's one. One way to fix this issue is to allow NOTIFY from the `dnsdist` +address on the slave side (for example with PowerDNS's `trusted-notification-proxy`) and +move the address check on `dnsdist`'s side: + +``` +> addAction(AndRule({OpcodeRule(DNSOpcode.Notify), NotRule(makeRule("192.168.1.0/24"))}), RCodeAction(dnsdist.REFUSED)) +``` + + All functions and types ----------------------- Within `dnsdist` several core object types exist: diff --git a/pdns/dnsdist-tcp.cc b/pdns/dnsdist-tcp.cc index 4adb768b78..c7b30984cc 100644 --- a/pdns/dnsdist-tcp.cc +++ b/pdns/dnsdist-tcp.cc @@ -22,6 +22,7 @@ #include "dnsdist.hh" #include "dnsdist-ecs.hh" +#include "dnsparser.hh" #include "ednsoptions.hh" #include "dolog.hh" #include "lock.hh" @@ -283,9 +284,6 @@ void* tcpClientThread(int pipefd) goto drop; } - if(dq.qtype == QType::AXFR || dq.qtype == QType::IXFR) // XXX fixme we really need to do better - break; - std::shared_ptr serverPool = getPool(*localPools, poolname); std::shared_ptr packetCache = nullptr; { @@ -383,6 +381,14 @@ void* tcpClientThread(int pipefd) goto retry; } + bool xfrStarted = false; + bool isXFR = (dq.qtype == QType::AXFR || dq.qtype == QType::IXFR); + if (isXFR) { + dq.skipCache = true; + } + + getpacket:; + if(!getNonBlockingMsgLen(dsock, &rlen, ds->tcpRecvTimeout)) { vinfolog("Downstream connection to %s died on us phase 2, getting a new one!", ds->getName()); close(dsock); @@ -390,6 +396,9 @@ void* tcpClientThread(int pipefd) sockets.erase(ds->remote); sockets[ds->remote]=dsock=setupTCPDownstream(ds); downstream_failures++; + if(xfrStarted) { + goto drop; + } goto retry; } @@ -442,6 +451,18 @@ void* tcpClientThread(int pipefd) break; } + if (isXFR && dh->rcode == 0 && dh->ancount != 0) { + if (xfrStarted == false) { + xfrStarted = true; + if (getRecordsOfTypeCount(response, responseLen, 1, QType::SOA) == 1) { + goto getpacket; + } + } + else if (getRecordsOfTypeCount(response, responseLen, 1, QType::SOA) == 0) { + goto getpacket; + } + } + g_stats.responses++; struct timespec answertime; gettime(&answertime); diff --git a/regression-tests.dnsdist/dnsdisttests.py b/regression-tests.dnsdist/dnsdisttests.py index 8184eed4a0..6ba23c672d 100644 --- a/regression-tests.dnsdist/dnsdisttests.py +++ b/regression-tests.dnsdist/dnsdisttests.py @@ -162,7 +162,7 @@ class DNSDistTest(unittest.TestCase): sock.close() @classmethod - def TCPResponder(cls, port, ignoreTrailing=False): + def TCPResponder(cls, port, ignoreTrailing=False, multipleResponses=False): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) try: @@ -182,12 +182,29 @@ class DNSDistTest(unittest.TestCase): response = cls._getResponse(request) if not response: + conn.close() continue wire = response.to_wire() conn.send(struct.pack("!H", len(wire))) conn.send(wire) + + while multipleResponses: + if cls._toResponderQueue.empty(): + break + + response = cls._toResponderQueue.get(True, cls._queueTimeout) + if not response: + break + + response = copy.copy(response) + response.id = request.id + wire = response.to_wire() + conn.send(struct.pack("!H", len(wire))) + conn.send(wire) + conn.close() + sock.close() @classmethod @@ -256,6 +273,46 @@ class DNSDistTest(unittest.TestCase): message = dns.message.from_wire(data) return (receivedQuery, message) + @classmethod + def sendTCPQueryWithMultipleResponses(cls, query, responses, useQueue=True, timeout=2.0, rawQuery=False): + if useQueue: + for response in responses: + cls._toResponderQueue.put(response, True, timeout) + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + if timeout: + sock.settimeout(timeout) + + sock.connect(("127.0.0.1", cls._dnsDistPort)) + messages = [] + + try: + if not rawQuery: + wire = query.to_wire() + else: + wire = query + + sock.send(struct.pack("!H", len(wire))) + sock.send(wire) + while True: + data = sock.recv(2) + if not data: + break + (datalen,) = struct.unpack("!H", data) + data = sock.recv(datalen) + messages.append(dns.message.from_wire(data)) + + except socket.timeout as e: + print("Timeout: %s" % (str(e))) + except socket.error as e: + print("Network error: %s" % (str(e))) + finally: + sock.close() + + receivedQuery = None + if useQueue and not cls._fromResponderQueue.empty(): + receivedQuery = cls._fromResponderQueue.get(True, timeout) + return (receivedQuery, messages) + def setUp(self): # This function is called before every tests diff --git a/regression-tests.dnsdist/test_AXFR.py b/regression-tests.dnsdist/test_AXFR.py new file mode 100644 index 0000000000..6f54dc3021 --- /dev/null +++ b/regression-tests.dnsdist/test_AXFR.py @@ -0,0 +1,279 @@ +#!/usr/bin/env python +import threading +import dns +from dnsdisttests import DNSDistTest + +class TestAXFR(DNSDistTest): + + # this test suite uses a different responder port + # because, contrary to the other ones, its + # TCP responder allows multiple responses and we don't want + # to mix things up. + _testServerPort = 5370 + _config_template = """ + newServer{address="127.0.0.1:%s"} + """ + @classmethod + def startResponders(cls): + print("Launching responders..") + + cls._UDPResponder = threading.Thread(name='UDP Responder', target=cls.UDPResponder, args=[cls._testServerPort]) + cls._UDPResponder.setDaemon(True) + cls._UDPResponder.start() + cls._TCPResponder = threading.Thread(name='TCP Responder', target=cls.TCPResponder, args=[cls._testServerPort, False, True]) + cls._TCPResponder.setDaemon(True) + cls._TCPResponder.start() + + _config_template = """ + newServer{address="127.0.0.1:%s"} + """ + + def testOneMessageAXFR(self): + """ + AXFR: One message + """ + name = 'one.axfr.tests.powerdns.com.' + query = dns.message.make_query(name, 'AXFR', 'IN') + response = dns.message.make_response(query) + soa = dns.rrset.from_text(name, + 60, + dns.rdataclass.IN, + dns.rdatatype.SOA, + 'ns.' + name + ' hostmaster.' + name + ' 1 3600 3600 3600 60') + response.answer.append(soa) + response.answer.append(dns.rrset.from_text(name, + 60, + dns.rdataclass.IN, + dns.rdatatype.A, + '192.0.2.1')) + response.answer.append(soa) + + (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response) + receivedQuery.id = query.id + self.assertEqual(query, receivedQuery) + self.assertEqual(response, receivedResponse) + + def testOneNoSOAAXFR(self): + """ + AXFR: One message, no SOA + """ + name = 'onenosoa.axfr.tests.powerdns.com.' + query = dns.message.make_query(name, 'AXFR', 'IN') + response = dns.message.make_response(query) + response.answer.append(dns.rrset.from_text(name, + 60, + dns.rdataclass.IN, + dns.rdatatype.A, + '192.0.2.1')) + + (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response) + receivedQuery.id = query.id + self.assertEqual(query, receivedQuery) + self.assertEqual(response, receivedResponse) + + def testFourMessagesAXFR(self): + """ + AXFR: Four messages + """ + name = 'four.axfr.tests.powerdns.com.' + query = dns.message.make_query(name, 'AXFR', 'IN') + responses = [] + soa = dns.rrset.from_text(name, + 60, + dns.rdataclass.IN, + dns.rdatatype.SOA, + 'ns.' + name + ' hostmaster.' + name + ' 1 3600 3600 3600 60') + response = dns.message.make_response(query) + response.answer.append(soa) + responses.append(response) + + response = dns.message.make_response(query) + response.answer.append(dns.rrset.from_text(name, + 60, + dns.rdataclass.IN, + dns.rdatatype.A, + '192.0.2.1')) + responses.append(response) + + response = dns.message.make_response(query) + rrset = dns.rrset.from_text(name, + 60, + dns.rdataclass.IN, + dns.rdatatype.AAAA, + '2001:DB8::1') + response.answer.append(rrset) + responses.append(response) + + response = dns.message.make_response(query) + rrset = dns.rrset.from_text(name, + 60, + dns.rdataclass.IN, + dns.rdatatype.TXT, + 'dummy') + response.answer.append(rrset) + responses.append(response) + + response = dns.message.make_response(query) + response.answer.append(soa) + responses.append(response) + + (receivedQuery, receivedResponses) = self.sendTCPQueryWithMultipleResponses(query, responses) + receivedQuery.id = query.id + self.assertEqual(query, receivedQuery) + self.assertEqual(len(receivedResponses), len(responses)) + + def testFourNoFinalSOAAXFR(self): + """ + AXFR: Four messages, no final SOA + """ + name = 'fournosoa.axfr.tests.powerdns.com.' + query = dns.message.make_query(name, 'AXFR', 'IN') + responses = [] + soa = dns.rrset.from_text(name, + 60, + dns.rdataclass.IN, + dns.rdatatype.SOA, + 'ns.' + name + ' hostmaster.' + name + ' 1 3600 3600 3600 60') + response = dns.message.make_response(query) + response.answer.append(soa) + responses.append(response) + + response = dns.message.make_response(query) + response.answer.append(dns.rrset.from_text(name, + 60, + dns.rdataclass.IN, + dns.rdatatype.A, + '192.0.2.1')) + responses.append(response) + + response = dns.message.make_response(query) + rrset = dns.rrset.from_text(name, + 60, + dns.rdataclass.IN, + dns.rdatatype.AAAA, + '2001:DB8::1') + response.answer.append(rrset) + responses.append(response) + + response = dns.message.make_response(query) + rrset = dns.rrset.from_text(name, + 60, + dns.rdataclass.IN, + dns.rdatatype.TXT, + 'dummy') + response.answer.append(rrset) + responses.append(response) + + (receivedQuery, receivedResponses) = self.sendTCPQueryWithMultipleResponses(query, responses) + receivedQuery.id = query.id + self.assertEqual(query, receivedQuery) + self.assertEqual(len(receivedResponses), len(responses)) + + def testFourNoFirstSOAAXFR(self): + """ + AXFR: Four messages, no SOA in the first one + """ + name = 'fournosoainfirst.axfr.tests.powerdns.com.' + query = dns.message.make_query(name, 'AXFR', 'IN') + responses = [] + soa = dns.rrset.from_text(name, + 60, + dns.rdataclass.IN, + dns.rdatatype.SOA, + 'ns.' + name + ' hostmaster.' + name + ' 1 3600 3600 3600 60') + response = dns.message.make_response(query) + response.answer.append(dns.rrset.from_text(name, + 60, + dns.rdataclass.IN, + dns.rdatatype.A, + '192.0.2.1')) + responses.append(response) + + response = dns.message.make_response(query) + rrset = dns.rrset.from_text(name, + 60, + dns.rdataclass.IN, + dns.rdatatype.AAAA, + '2001:DB8::1') + response.answer.append(soa) + response.answer.append(rrset) + responses.append(response) + + response = dns.message.make_response(query) + rrset = dns.rrset.from_text('dummy.' + name, + 60, + dns.rdataclass.IN, + dns.rdatatype.AAAA, + '2001:DB8::1') + response.answer.append(rrset) + responses.append(response) + + response = dns.message.make_response(query) + rrset = dns.rrset.from_text(name, + 60, + dns.rdataclass.IN, + dns.rdatatype.TXT, + 'dummy') + response.answer.append(rrset) + response.answer.append(soa) + responses.append(response) + + (receivedQuery, receivedResponses) = self.sendTCPQueryWithMultipleResponses(query, responses) + receivedQuery.id = query.id + self.assertEqual(query, receivedQuery) + self.assertEqual(len(receivedResponses), 1) + + def testFourLastSOAInSecondAXFR(self): + """ + AXFR: Four messages, SOA in the first one and the second one + """ + name = 'foursecondsoainsecond.axfr.tests.powerdns.com.' + query = dns.message.make_query(name, 'AXFR', 'IN') + responses = [] + soa = dns.rrset.from_text(name, + 60, + dns.rdataclass.IN, + dns.rdatatype.SOA, + 'ns.' + name + ' hostmaster.' + name + ' 1 3600 3600 3600 60') + + response = dns.message.make_response(query) + response.answer.append(soa) + response.answer.append(dns.rrset.from_text(name, + 60, + dns.rdataclass.IN, + dns.rdatatype.A, + '192.0.2.1')) + responses.append(response) + + response = dns.message.make_response(query) + response.answer.append(soa) + rrset = dns.rrset.from_text(name, + 60, + dns.rdataclass.IN, + dns.rdatatype.AAAA, + '2001:DB8::1') + response.answer.append(rrset) + responses.append(response) + + response = dns.message.make_response(query) + rrset = dns.rrset.from_text('dummy.' + name, + 60, + dns.rdataclass.IN, + dns.rdatatype.AAAA, + '2001:DB8::1') + response.answer.append(rrset) + responses.append(response) + + response = dns.message.make_response(query) + rrset = dns.rrset.from_text(name, + 60, + dns.rdataclass.IN, + dns.rdatatype.TXT, + 'dummy') + response.answer.append(rrset) + responses.append(response) + + (receivedQuery, receivedResponses) = self.sendTCPQueryWithMultipleResponses(query, responses) + receivedQuery.id = query.id + self.assertEqual(query, receivedQuery) + self.assertEqual(len(receivedResponses), 2)