From: Peter van Dijk Date: Tue, 15 Aug 2023 17:26:55 +0000 (+0200) Subject: ixfrdist: handle incoming NOTIFY packets X-Git-Tag: rec-5.0.0-beta1~38^2~5 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=0669936360ee390caabb2d63912be94ef04a4226;p=thirdparty%2Fpdns.git ixfrdist: handle incoming NOTIFY packets --- diff --git a/pdns/ixfrdist.cc b/pdns/ixfrdist.cc index fde9933454..52b6599d1c 100644 --- a/pdns/ixfrdist.cc +++ b/pdns/ixfrdist.cc @@ -20,6 +20,7 @@ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "dns.hh" +#include "dnsparser.hh" #ifdef HAVE_CONFIG_H #include "config.h" #endif @@ -152,6 +153,10 @@ static map g_domainConfigs; // Map domains and their data static LockGuarded>> g_soas; +// Queue of received NOTIFYs, already verified against their master IPs +// Lazily implemented as a set +static LockGuarded> g_notifiesReceived; + // Condition variable for TCP handling static std::condition_variable g_tcpHandlerCV; static std::queue> g_tcpRequestFDs; @@ -161,7 +166,8 @@ namespace po = boost::program_options; static bool g_exiting = false; -static NetmaskGroup g_acl; +static NetmaskGroup g_acl; // networks that can QUERY us +static NetmaskGroup g_notifySources; // networks (well, IPs) that can NOTIFY us static bool g_compress = false; static ixfrdistStats g_stats; @@ -348,7 +354,9 @@ static void updateThread(const string& workdir, const uint16_t& keep, const uint refresh = std::min(refresh, domainConfig.second.maxSOARefresh); } } - if (now - zoneLastCheck < refresh) { + + + if (now - zoneLastCheck < refresh && g_notifiesReceived.lock()->erase(domain) == 0) { continue; } @@ -476,12 +484,54 @@ static void updateThread(const string& workdir, const uint16_t& keep, const uint } /* updateThread */ enum class ResponseType { - // Unknown, + Unknown, ValidQuery, RefusedOpcode, - RefusedQuery + RefusedQuery, + EmptyNoError }; +static ResponseType maybeHandleNotify(const MOADNSParser& mdp, const ComboAddress& saddr, const string& logPrefix="") { + if (mdp.d_header.opcode != Opcode::Notify) { + return ResponseType::Unknown; + } + + g_log<second.masters; + + bool masterFound = false; + + auto saddrPort0 = saddr; + saddrPort0.setPort(0); + + for(const auto &master : masters) { + auto masterPort0 = master; + masterPort0.setPort(0); + cerr< info_msg; @@ -490,7 +540,7 @@ static ResponseType checkQuery(const MOADNSParser& mdp, const ComboAddress& sadd g_log< that represents the full empty NOERROR response. + * QNAME is read from mdp. + */ +static bool makeEmptyNoErrorPacket(const MOADNSParser& mdp, vector& packet) { + + auto zoneInfo = getCurrentZoneInfo(mdp.d_qname); + if (zoneInfo == nullptr) { + return false; + } + + DNSPacketWriter pw(packet, mdp.d_qname, mdp.d_qtype); + pw.getHeader()->opcode = mdp.d_header.opcode; + pw.getHeader()->id = mdp.d_header.id; + pw.getHeader()->rd = mdp.d_header.rd; + pw.getHeader()->qr = 1; + pw.getHeader()->aa = 1; + + pw.commit(); + + return true; +} + /* * Returns a vector that represents the full positive response to a SOA * query. QNAME is read from mdp. @@ -549,6 +622,7 @@ static bool makeSOAPacket(const MOADNSParser& mdp, vector& packet) { } DNSPacketWriter pw(packet, mdp.d_qname, mdp.d_qtype); + pw.getHeader()->opcode = mdp.d_header.opcode; pw.getHeader()->id = mdp.d_header.id; pw.getHeader()->rd = mdp.d_header.rd; pw.getHeader()->qr = 1; @@ -567,6 +641,7 @@ static bool makeSOAPacket(const MOADNSParser& mdp, vector& packet) { */ static bool makeRefusedPacket(const MOADNSParser& mdp, vector& packet) { DNSPacketWriter pw(packet, mdp.d_qname, mdp.d_qtype); + pw.getHeader()->opcode = mdp.d_header.opcode; pw.getHeader()->id = mdp.d_header.id; pw.getHeader()->rd = mdp.d_header.rd; pw.getHeader()->qr = 1; @@ -581,11 +656,11 @@ static bool makeRefusedPacket(const MOADNSParser& mdp, vector& packet) */ static bool makeNotimpPacket(const MOADNSParser& mdp, vector& packet) { DNSPacketWriter pw(packet, mdp.d_qname, mdp.d_qtype); + pw.getHeader()->opcode = mdp.d_header.opcode; pw.getHeader()->id = mdp.d_header.id; pw.getHeader()->rd = mdp.d_header.rd; pw.getHeader()->qr = 1; pw.getHeader()->rcode = RCode::NotImp; - pw.getHeader()->opcode = mdp.d_header.opcode; return true; } @@ -814,7 +889,11 @@ static bool handleIXFR(int fd, const MOADNSParser& mdp, const shared_ptr packet; + + ResponseType respt = ResponseType::Unknown; + + if (allowedByACL(saddr, true)) { + respt = maybeHandleNotify(mdp, saddr); + } + else if (!allowedByACL(saddr)) { g_log< packet; - auto respt = checkQuery(mdp, saddr); + if (respt == ResponseType::Unknown) { + // query was not handled yet (so not a valid NOTIFY) + respt = checkQuery(mdp, saddr); + } if (respt == ResponseType::ValidQuery) { /* RFC 1995 Section 2 * Transport of a query may be by either UDP or TCP. If an IXFR query @@ -865,6 +953,8 @@ try */ g_stats.incrementSOAinQueries(mdp.d_qname); // FIXME: this also counts IXFR queries (but the response is the same as to a SOA query) makeSOAPacket(mdp, packet); + } else if (respt == ResponseType::EmptyNoError) { + makeEmptyNoErrorPacket(mdp, packet); } else if (respt == ResponseType::RefusedQuery) { g_stats.incrementUnknownDomainInQueries(mdp.d_qname); makeRefusedPacket(mdp, packet); @@ -902,7 +992,9 @@ static void handleTCPRequest(int fd, boost::any&) { return; } - if (!allowedByACL(saddr)) { + // we allow the connection if this is a legit client or a legit NOTIFY source + // need to check per-operation later + if (!allowedByACL(saddr) && !allowedByACL(saddr, true)) { g_log< packet; + if (respt == ResponseType::Unknown) { + respt = checkQuery(mdp, saddr, false, prefix); + } + + if (respt != ResponseType::ValidQuery && respt != ResponseType::EmptyNoError) { // on TCP, we currently do not bother with sending useful errors + close(cfd); + continue; + } + + vector packet; + + if (respt == ResponseType::EmptyNoError) { + bool ret = makeEmptyNoErrorPacket(mdp, packet); + if (!ret) { + close(cfd); + continue; + } + sendPacketOverTCP(cfd, packet); + } + else if (mdp.d_qtype == QType::SOA) { bool ret = makeSOAPacket(mdp, packet); if (!ret) { close(cfd); @@ -1164,6 +1278,10 @@ static bool parseAndCheckConfig(const string& configpath, YAML::Node& config) { continue; } domain["master"].as(); + + auto notifySource = domain["master"].as(); + + g_notifySources.addMask(notifySource); } catch (const runtime_error &e) { g_log<()<<"' master address: "< parseConfiguration(int argc, char** g_log<(); if (g_compress) { diff --git a/regression-tests.ixfrdist/test_IXFR.py b/regression-tests.ixfrdist/test_IXFR.py index c0788f874a..159d85d4fb 100644 --- a/regression-tests.ixfrdist/test_IXFR.py +++ b/regression-tests.ixfrdist/test_IXFR.py @@ -26,7 +26,7 @@ newrecord.example. 8484 A 192.0.2.42 """, 3: """ $ORIGIN example. -@ 86400 SOA foo bar 3 2 3 4 5 +@ 86400 SOA foo bar 3 1500 3 4 5 @ 4242 NS ns1.example. @ 4242 NS ns2.example. ns1.example. 4242 A 192.0.2.1 @@ -72,11 +72,17 @@ class IXFRDistBasicTest(IXFRDistTest): def tearDownClass(cls): cls.tearDownIXFRDist() - def waitUntilCorrectSerialIsLoaded(self, serial, timeout=10): + def waitUntilCorrectSerialIsLoaded(self, serial, timeout=10, notify=False): global xfrServer xfrServer.moveToSerial(serial) + if notify: + notif = dns.message.make_query('example.', 'SOA') + notif.set_opcode(dns.opcode.NOTIFY) + notify_response = self.sendUDPQuery(notif) + assert notify_response.rcode() == dns.rcode.NOERROR + def get_current_serial(): query = dns.message.make_query('example.', 'SOA') response_message = self.sendUDPQuery(query) @@ -227,7 +233,7 @@ class IXFRDistBasicTest(IXFRDistTest): self.checkIXFR(2,3) self.checkIXFR(1,3) - self.waitUntilCorrectSerialIsLoaded(4) + self.waitUntilCorrectSerialIsLoaded(serial=4, timeout=10, notify=True) self.checkFullZone(4) self.checkIXFR(3,4) self.checkIXFR(2,4)