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:
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:
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
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
--- /dev/null
+#!/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)