]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
rec: Add support for NOTIFY operations to wipe cache entries
authorKevin P. Fleming <kevin@km6g.us>
Fri, 12 Nov 2021 12:20:19 +0000 (07:20 -0500)
committerKevin P. Fleming <kevin@km6g.us>
Fri, 12 Nov 2021 12:20:19 +0000 (07:20 -0500)
NOTIFY operations can be sent to trigger removal of cache entries which
match the zone specified in the operation. All entries, regardless of
type, in or below the specified zone, are removed.  Control over
permission to send such operations is provided by an ACL, and control over
zones which can be wiped is provided by a new configuration setting.

The default configuration ignores all NOTIFY operations.

This patch adds:

* 'allow-notify-from' and 'allow-notify-from-file' settings, operating
  almost identically to 'allow-from' and 'allow-from-file' (the only
  difference being the default value).

* 'allow-notify-for' and 'allow-notify-for-file' settings, which provide
  a list of zones for which NOTIFY operations are allowed.

* modification to 'forward-zones-file' setting, allowing zones specified
  there to optionally allow NOTIFY operations.

* 'source-disallowed-notify' metric, counting the number of NOTIFY operations
  which have been denied by the ACL.

* 'zone-disallowed-notify' metric, counting the number of NOTIFY operations
  which have been denied by the zone list.

* API support for modifying 'allow-notify-from' ACL.

* Regression tests for new ACL settings.

12 files changed:
pdns/pdns_recursor.cc
pdns/rec-snmp.cc
pdns/rec_channel_rec.cc
pdns/recursordist/RECURSOR-MIB.txt
pdns/recursordist/docs/http-api/endpoint-servers-config.rst
pdns/recursordist/docs/metrics.rst
pdns/recursordist/docs/settings.rst
pdns/reczones.cc
pdns/syncres.hh
pdns/ws-recursor.cc
regression-tests.api/runtests.py
regression-tests.api/test_RecursorConfig.py

index 346b437f33cc584d06e72c6c838b83563abf0236..f3ac0a08dc84491c69d44b327d21136b62dc1f3e 100644 (file)
@@ -143,6 +143,8 @@ thread_local FDMultiplexer* t_fdm{nullptr};
 thread_local std::unique_ptr<addrringbuf_t> t_remotes, t_servfailremotes, t_largeanswerremotes, t_bogusremotes;
 thread_local std::unique_ptr<boost::circular_buffer<pair<DNSName, uint16_t> > > t_queryring, t_servfailqueryring, t_bogusqueryring;
 thread_local std::shared_ptr<NetmaskGroup> t_allowFrom;
+thread_local std::shared_ptr<NetmaskGroup> t_allowNotifyFrom;
+thread_local std::shared_ptr<notifyset_t> t_allowNotifyFor;
 #ifdef NOD_ENABLED
 thread_local std::shared_ptr<nod::NODDB> t_nodDBp;
 thread_local std::shared_ptr<nod::UniqueResponseDB> t_udrDBp;
@@ -204,6 +206,8 @@ static set<int> g_fromtosockets; // listen sockets that use 'sendfromto()' mecha
 static std::atomic<uint32_t> counter;
 static std::shared_ptr<SyncRes::domainmap_t> g_initialDomainMap; // new threads needs this to be setup
 static std::shared_ptr<NetmaskGroup> g_initialAllowFrom; // new thread needs to be setup with this
+static std::shared_ptr<NetmaskGroup> g_initialAllowNotifyFrom; // new threads need this to be setup
+static std::shared_ptr<notifyset_t> g_initialAllowNotifyFor; // new threads need this to be setup
 static NetmaskGroup g_XPFAcl;
 static NetmaskGroup g_proxyProtocolACL;
 static NetmaskGroup g_paddingFrom;
@@ -1602,6 +1606,21 @@ static bool answerIsNOData(uint16_t requestedType, int rcode, const std::vector<
   return true;
 }
 
+static bool isAllowNotifyForZone(DNSName qname)
+{
+  if (t_allowNotifyFor->empty()) {
+    return false;
+  }
+
+  notifyset_t::const_iterator ret;
+  do {
+    ret = t_allowNotifyFor->find(qname);
+    if (ret != t_allowNotifyFor->end())
+      return true;
+  } while (qname.chopOff());
+  return false;
+}
+
 static void startDoResolve(void *p)
 {
   auto dc=std::unique_ptr<DNSComboWriter>(reinterpret_cast<DNSComboWriter*>(p));
@@ -1693,7 +1712,7 @@ static void startDoResolve(void *p)
     checkFrameStreamExport(luaconfsLocal);
 #endif
 
-    DNSPacketWriter pw(packet, dc->d_mdp.d_qname, dc->d_mdp.d_qtype, dc->d_mdp.d_qclass);
+    DNSPacketWriter pw(packet, dc->d_mdp.d_qname, dc->d_mdp.d_qtype, dc->d_mdp.d_qclass, dc->d_mdp.d_header.opcode);
 
     pw.getHeader()->aa=0;
     pw.getHeader()->ra=1;
@@ -1779,7 +1798,7 @@ static void startDoResolve(void *p)
     dq.extendedErrorExtra = &dc->d_extendedErrorExtra;
     dq.meta = std::move(dc->d_meta);
 
-    if(ednsExtRCode != 0) {
+    if(ednsExtRCode != 0 || dc->d_mdp.d_header.opcode == Opcode::Notify) {
       goto sendit;
     }
 
@@ -2614,6 +2633,27 @@ static bool checkForCacheHit(bool qnameParsed, unsigned int tag, const string& d
   return cacheHit;
 }
 
+static void* pleaseWipeCaches(const DNSName& canon, bool subtree, uint16_t qtype)
+{
+  auto res = wipeCaches(canon, subtree, qtype);
+  g_log<<Logger::Info<<"Wiped caches for "<<canon<<": "<<res.record_count<<" records; "<<res.negative_record_count<<" negative records; "<<res.packet_count<<" packets"<<endl;
+  return nullptr;
+}
+
+static void requestWipeCaches(const DNSName& canon)
+{
+  // send a message to the handler thread asking it
+  // to wipe all of the caches
+  ThreadMSG* tmsg = new ThreadMSG();
+  tmsg->func = [=]{ return pleaseWipeCaches(canon, true, 0xffff); };
+  tmsg->wantAnswer = false;
+  if(write(s_threadInfos[0].pipes.writeToThread, &tmsg, sizeof(tmsg)) != sizeof(tmsg)) {
+    delete tmsg;
+
+    unixDie("write to thread pipe returned wrong size or error");
+  }
+}
+
 static void handleRunningTCPQuestion(int fd, FDMultiplexer::funcparam_t& var)
 {
   shared_ptr<TCPConnection> conn=boost::any_cast<shared_ptr<TCPConnection> >(var);
@@ -2792,7 +2832,7 @@ static void handleRunningTCPQuestion(int fd, FDMultiplexer::funcparam_t& var)
       checkFrameStreamExport(luaconfsLocal);
 #endif
 
-      if(needECS || needXPF || (t_pdl && (t_pdl->d_gettag_ffi || t_pdl->d_gettag))) {
+      if(needECS || needXPF || (t_pdl && (t_pdl->d_gettag_ffi || t_pdl->d_gettag)) || dc->d_mdp.d_header.opcode == Opcode::Notify) {
 
         try {
           EDNSOptionViewMap ednsOptions;
@@ -2882,10 +2922,10 @@ static void handleRunningTCPQuestion(int fd, FDMultiplexer::funcparam_t& var)
         terminateTCPConnection(fd);
         return;
       }
-      if (dc->d_mdp.d_header.opcode) {
+      if (dc->d_mdp.d_header.opcode != Opcode::Query && dc->d_mdp.d_header.opcode != Opcode::Notify) {
         g_stats.ignoredCount++;
         if (g_logCommonErrors) {
-          g_log<<Logger::Error<<"Ignoring non-query opcode from TCP client "<< dc->getRemote() <<" on server socket!"<<endl;
+          g_log<<Logger::Error<<"Ignoring unsupported opcode "<<Opcode::to_s(dc->d_mdp.d_header.opcode)<<" from TCP client "<< dc->getRemote() <<" on server socket!"<<endl;
         }
         sendErrorOverTCP(dc, RCode::NotImp);
         return;
@@ -2903,50 +2943,85 @@ static void handleRunningTCPQuestion(int fd, FDMultiplexer::funcparam_t& var)
         ++g_stats.qcounter;
         ++g_stats.tcpqcounter;
 
+        if(dc->d_mdp.d_header.opcode == Opcode::Notify) {
+          if(!t_allowNotifyFrom || !t_allowNotifyFrom->match(dc->d_source)) {
+            if(!g_quiet) {
+              g_log<<Logger::Error<<"["<<MT->getTid()<<"] dropping TCP NOTIFY from "<<dc->d_source.toString()<<", address not matched by allow-notify-from"<<endl;
+            }
+
+            g_stats.sourceDisallowedNotify++;
+            return;
+          }
+
+          if(!isAllowNotifyForZone(qname)) {
+            if(!g_quiet) {
+              g_log<<Logger::Error<<"["<<MT->getTid()<<"] dropping TCP NOTIFY from "<<dc->d_source.toString()<<", for "<<qname.toLogString()<<", zone not matched by allow-notify-for"<<endl;
+            }
+
+            g_stats.zoneDisallowedNotify++;
+            return;
+          }
+        }
+
         string response;
         RecursorPacketCache::OptPBData pbData{boost::none};
 
-        /* It might seem like a good idea to skip the packet cache lookup if we know that the answer is not cacheable,
-           but it means that the hash would not be computed. If some script decides at a later time to mark back the answer
-           as cacheable we would cache it with a wrong tag, so better safe than sorry. */
-        dc->d_eventTrace.add(RecEventTrace::PCacheCheck);
-        bool cacheHit = checkForCacheHit(qnameParsed, dc->d_tag, conn->data, qname, qtype, qclass, g_now, response, dc->d_qhash, pbData, true, dc->d_source);
-        dc->d_eventTrace.add(RecEventTrace::PCacheCheck, cacheHit, false);
+        if(dc->d_mdp.d_header.opcode == Opcode::Query) {
+          /* It might seem like a good idea to skip the packet cache lookup if we know that the answer is not cacheable,
+             but it means that the hash would not be computed. If some script decides at a later time to mark back the answer
+             as cacheable we would cache it with a wrong tag, so better safe than sorry. */
+          dc->d_eventTrace.add(RecEventTrace::PCacheCheck);
+          bool cacheHit = checkForCacheHit(qnameParsed, dc->d_tag, conn->data, qname, qtype, qclass, g_now, response, dc->d_qhash, pbData, true, dc->d_source);
+          dc->d_eventTrace.add(RecEventTrace::PCacheCheck, cacheHit, false);
+
+          if (cacheHit) {
+            if (!g_quiet) {
+              g_log<<Logger::Notice<<t_id<< " TCP question answered from packet cache tag="<<dc->d_tag<<" from "<<dc->d_source.toStringWithPort()<<(dc->d_source != dc->d_remote ? " (via "+dc->d_remote.toStringWithPort()+")" : "")<<endl;
+            }
 
-        if (cacheHit) {
+            bool hadError = sendResponseOverTCP(dc, response);
+            finishTCPReply(dc, hadError, false);
+            struct timeval now;
+            Utility::gettimeofday(&now, nullptr);
+            uint64_t spentUsec = uSec(now - start);
+            g_stats.cumulativeAnswers(spentUsec);
+            dc->d_eventTrace.add(RecEventTrace::AnswerSent);
+
+            if (t_protobufServers && dc->d_logResponse && !(luaconfsLocal->protobufExportConfig.taggedOnly && pbData && !pbData->d_tagged)) {
+              struct timeval tv{0, 0};
+              protobufLogResponse(dh, luaconfsLocal, pbData, tv, true, dc->d_source, dc->d_destination, dc->d_ednssubnet, dc->d_uuid, dc->d_requestorId, dc->d_deviceId, dc->d_deviceName, dc->d_meta, dc->d_eventTrace);
+            }
 
+            if (dc->d_eventTrace.enabled() && SyncRes::s_event_trace_enabled & SyncRes::event_trace_to_log) {
+              g_log << Logger::Info << dc->d_eventTrace.toString() << endl;
+            }
+          } // cache hit
+        } // query opcode
+
+        if(dc->d_mdp.d_header.opcode == Opcode::Notify) {
           if (!g_quiet) {
-            g_log<<Logger::Notice<<t_id<< " TCP question answered from packet cache tag="<<dc->d_tag<<" from "<<dc->d_source.toStringWithPort()<<(dc->d_source != dc->d_remote ? " (via "+dc->d_remote.toStringWithPort()+")" : "")<<endl;
+            g_log<<Logger::Notice<<t_id<< " got NOTIFY for "<<qname.toLogString()<<" from "<<dc->d_source.toStringWithPort()<<(dc->d_source != dc->d_remote ? " (via "+dc->d_remote.toStringWithPort()+")" : "")<<endl;
           }
 
-          bool hadError = sendResponseOverTCP(dc, response);
-          finishTCPReply(dc, hadError, false);
-          struct timeval now;
-          Utility::gettimeofday(&now, nullptr);
-          uint64_t spentUsec = uSec(now - start);
-          g_stats.cumulativeAnswers(spentUsec);
-          dc->d_eventTrace.add(RecEventTrace::AnswerSent);
-
-          if (t_protobufServers && dc->d_logResponse && !(luaconfsLocal->protobufExportConfig.taggedOnly && pbData && !pbData->d_tagged)) {
-            struct timeval tv{0, 0};
-            protobufLogResponse(dh, luaconfsLocal, pbData, tv, true, dc->d_source, dc->d_destination, dc->d_ednssubnet, dc->d_uuid, dc->d_requestorId, dc->d_deviceId, dc->d_deviceName, dc->d_meta, dc->d_eventTrace);
-          }
+          requestWipeCaches(qname);
 
-          if (dc->d_eventTrace.enabled() && SyncRes::s_event_trace_enabled & SyncRes::event_trace_to_log) {
-            g_log << Logger::Info << dc->d_eventTrace.toString() << endl;
-          }
+          // the operation will now be treated as a Query, generating
+          // a normal response, as the rest of the code does not
+          // check dh->opcode, but we need to ensure that the response
+          // to this request does not get put into the packet cache
+          dc->d_variable = true;
+        }
+
+        // setup for startDoResolve() in an mthread
+        ++conn->d_requestsInFlight;
+        if (conn->d_requestsInFlight >= TCPConnection::s_maxInFlight) {
+          t_fdm->removeReadFD(fd); // should no longer awake ourselves when there is data to read
         } else {
-          // No cache hit, setup for startDoResolve() in an mthread
-          ++conn->d_requestsInFlight;
-          if (conn->d_requestsInFlight >= TCPConnection::s_maxInFlight) {
-            t_fdm->removeReadFD(fd); // should no longer awake ourselves when there is data to read
-          } else {
-            Utility::gettimeofday(&g_now, nullptr); // needed?
-            struct timeval ttd = g_now;
-            t_fdm->setReadTTD(fd, ttd, g_tcpTimeout);
-          }
-          MT->makeThread(startDoResolve, dc.release()); // deletes dc
-        } // Cache hit or not
+          Utility::gettimeofday(&g_now, nullptr); // needed?
+          struct timeval ttd = g_now;
+          t_fdm->setReadTTD(fd, ttd, g_tcpTimeout);
+        }
+        MT->makeThread(startDoResolve, dc.release()); // deletes dc
       } // good query
     } // read full query
   } // reading query
@@ -3086,8 +3161,8 @@ static string* doProcessUDPQuestion(const std::string& question, const ComboAddr
   bool variable = false;
   bool followCNAMEs = false;
   bool responsePaddingDisabled = false;
+  DNSName qname;
   try {
-    DNSName qname;
     uint16_t qtype=0;
     uint16_t qclass=0;
     bool qnameParsed=false;
@@ -3103,7 +3178,7 @@ static string* doProcessUDPQuestion(const std::string& question, const ComboAddr
     */
 #endif
 
-    if(needECS || needXPF || (t_pdl && (t_pdl->d_gettag || t_pdl->d_gettag_ffi))) {
+    if(needECS || needXPF || (t_pdl && (t_pdl->d_gettag || t_pdl->d_gettag_ffi)) || dh->opcode == Opcode::Notify) {
       try {
         EDNSOptionViewMap ednsOptions;
         bool xpfFound = false;
@@ -3157,45 +3232,48 @@ static string* doProcessUDPQuestion(const std::string& question, const ComboAddr
     if (ctag == 0 && !responsePaddingDisabled && g_paddingFrom.match(fromaddr)) {
       ctag = g_paddingTag;
     }
-    /* It might seem like a good idea to skip the packet cache lookup if we know that the answer is not cacheable,
-       but it means that the hash would not be computed. If some script decides at a later time to mark back the answer
-       as cacheable we would cache it with a wrong tag, so better safe than sorry. */
-    eventTrace.add(RecEventTrace::PCacheCheck);
-    bool cacheHit = checkForCacheHit(qnameParsed, ctag, question, qname, qtype, qclass, g_now, response, qhash, pbData, false, source);
-    eventTrace.add(RecEventTrace::PCacheCheck, cacheHit, false);
-    if (cacheHit) {
-      if (!g_quiet) {
-        g_log<<Logger::Notice<<t_id<< " question answered from packet cache tag="<<ctag<<" from "<<source.toStringWithPort()<<(source != fromaddr ? " (via "+fromaddr.toStringWithPort()+")" : "")<<endl;
-      }
-      struct msghdr msgh;
-      struct iovec iov;
-      cmsgbuf_aligned cbuf;
-      fillMSGHdr(&msgh, &iov, &cbuf, 0, (char*)response.c_str(), response.length(), const_cast<ComboAddress*>(&fromaddr));
-      msgh.msg_control=NULL;
 
-      if(g_fromtosockets.count(fd)) {
-        addCMsgSrcAddr(&msgh, &cbuf, &destaddr, 0);
-      }
-      int sendErr = sendOnNBSocket(fd, &msgh);
-      eventTrace.add(RecEventTrace::AnswerSent);
+    if(dh->opcode == Opcode::Query) {
+      /* It might seem like a good idea to skip the packet cache lookup if we know that the answer is not cacheable,
+         but it means that the hash would not be computed. If some script decides at a later time to mark back the answer
+         as cacheable we would cache it with a wrong tag, so better safe than sorry. */
+      eventTrace.add(RecEventTrace::PCacheCheck);
+      bool cacheHit = checkForCacheHit(qnameParsed, ctag, question, qname, qtype, qclass, g_now, response, qhash, pbData, false, source);
+      eventTrace.add(RecEventTrace::PCacheCheck, cacheHit, false);
+      if (cacheHit) {
+        if (!g_quiet) {
+          g_log<<Logger::Notice<<t_id<< " question answered from packet cache tag="<<ctag<<" from "<<source.toStringWithPort()<<(source != fromaddr ? " (via "+fromaddr.toStringWithPort()+")" : "")<<endl;
+        }
+        struct msghdr msgh;
+        struct iovec iov;
+        cmsgbuf_aligned cbuf;
+        fillMSGHdr(&msgh, &iov, &cbuf, 0, (char*)response.c_str(), response.length(), const_cast<ComboAddress*>(&fromaddr));
+        msgh.msg_control=NULL;
+
+        if(g_fromtosockets.count(fd)) {
+          addCMsgSrcAddr(&msgh, &cbuf, &destaddr, 0);
+        }
+        int sendErr = sendOnNBSocket(fd, &msgh);
+        eventTrace.add(RecEventTrace::AnswerSent);
 
-      if (t_protobufServers && logResponse && !(luaconfsLocal->protobufExportConfig.taggedOnly && pbData && !pbData->d_tagged)) {
-        protobufLogResponse(dh, luaconfsLocal, pbData, tv, false, source, destination, ednssubnet, uniqueId, requestorId, deviceId, deviceName, meta, eventTrace);
-      }
+        if (t_protobufServers && logResponse && !(luaconfsLocal->protobufExportConfig.taggedOnly && pbData && !pbData->d_tagged)) {
+          protobufLogResponse(dh, luaconfsLocal, pbData, tv, false, source, destination, ednssubnet, uniqueId, requestorId, deviceId, deviceName, meta, eventTrace);
+        }
 
-      if (eventTrace.enabled() && SyncRes::s_event_trace_enabled & SyncRes::event_trace_to_log) {
-        g_log << Logger::Info << eventTrace.toString() << endl;
-      }
-      if (sendErr && g_logCommonErrors) {
-        g_log << Logger::Warning << "Sending UDP reply to client " << source.toStringWithPort()
-              << (source != fromaddr ? " (via " + fromaddr.toStringWithPort() + ")" : "") << " failed with: "
-              << strerror(sendErr) << endl;
+        if (eventTrace.enabled() && SyncRes::s_event_trace_enabled & SyncRes::event_trace_to_log) {
+          g_log << Logger::Info << eventTrace.toString() << endl;
+        }
+        if (sendErr && g_logCommonErrors) {
+          g_log << Logger::Warning << "Sending UDP reply to client " << source.toStringWithPort()
+                << (source != fromaddr ? " (via " + fromaddr.toStringWithPort() + ")" : "") << " failed with: "
+                << strerror(sendErr) << endl;
+        }
+        struct timeval now;
+        Utility::gettimeofday(&now, nullptr);
+        uint64_t spentUsec = uSec(now - tv);
+        g_stats.cumulativeAnswers(spentUsec);
+        return 0;
       }
-      struct timeval now;
-      Utility::gettimeofday(&now, nullptr);
-      uint64_t spentUsec = uSec(now - tv);
-      g_stats.cumulativeAnswers(spentUsec);
-      return 0;
     }
   }
   catch (const std::exception& e) {
@@ -3211,13 +3289,36 @@ static string* doProcessUDPQuestion(const std::string& question, const ComboAddr
     eventTrace.add(RecEventTrace::LuaIPFilter, ipf, false);
     if (ipf) {
       if (!g_quiet) {
-       g_log<<Logger::Notice<<t_id<<" ["<<MT->getTid()<<"/"<<MT->numProcesses()<<"] DROPPED question from "<<source.toStringWithPort()<<(source != fromaddr ? " (via "+fromaddr.toStringWithPort()+")" : "")<<" based on policy"<<endl;
+        g_log<<Logger::Notice<<t_id<<" ["<<MT->getTid()<<"/"<<MT->numProcesses()<<"] DROPPED question from "<<source.toStringWithPort()<<(source != fromaddr ? " (via "+fromaddr.toStringWithPort()+")" : "")<<" based on policy"<<endl;
       }
       g_stats.policyDrops++;
       return 0;
     }
   }
 
+  if(dh->opcode == Opcode::Notify) {
+    if(!isAllowNotifyForZone(qname)) {
+      if(!g_quiet) {
+        g_log<<Logger::Error<<"["<<MT->getTid()<<"] dropping UDP NOTIFY from "<<source.toStringWithPort()<<(source != fromaddr ? " (via "+fromaddr.toStringWithPort()+")" : "")<<", for "<<qname.toLogString()<<", zone not matched by allow-notify-for"<<endl;
+      }
+
+      g_stats.zoneDisallowedNotify++;
+      return 0;
+    }
+
+    if (!g_quiet) {
+      g_log<<Logger::Notice<<t_id<< " got NOTIFY for "<<qname.toLogString()<<" from "<<source.toStringWithPort()<<(source != fromaddr ? " (via "+fromaddr.toStringWithPort()+")" : "")<<endl;
+    }
+
+    requestWipeCaches(qname);
+
+    // the operation will now be treated as a Query, generating
+    // a normal response, as the rest of the code does not
+    // check dh->opcode, but we need to ensure that the response
+    // to this request does not get put into the packet cache
+    variable = true;
+  }
+
   if(MT->numProcesses() > g_maxMThreads) {
     if(!g_quiet)
       g_log<<Logger::Notice<<t_id<<" ["<<MT->getTid()<<"/"<<MT->numProcesses()<<"] DROPPED question from "<<source.toStringWithPort()<<(source != fromaddr ? " (via "+fromaddr.toStringWithPort()+")" : "")<<", over capacity"<<endl;
@@ -3375,10 +3476,10 @@ static void handleNewUDPQuestion(int fd, FDMultiplexer::funcparam_t& var)
             g_log<<Logger::Error<<"Ignoring answer from "<<fromaddr.toString()<<" on server socket!"<<endl;
           }
         }
-        else if(dh->opcode) {
+        else if(dh->opcode != Opcode::Query && dh->opcode != Opcode::Notify) {
           g_stats.ignoredCount++;
           if(g_logCommonErrors) {
-            g_log<<Logger::Error<<"Ignoring non-query opcode "<<dh->opcode<<" from "<<fromaddr.toString()<<" on server socket!"<<endl;
+            g_log<<Logger::Error<<"Ignoring unsupported opcode "<<Opcode::to_s(dh->opcode)<<" from "<<fromaddr.toString()<<" on server socket!"<<endl;
           }
         }
         else if (dh->qdcount == 0) {
@@ -3388,6 +3489,17 @@ static void handleNewUDPQuestion(int fd, FDMultiplexer::funcparam_t& var)
           }
         }
         else {
+          if(dh->opcode == Opcode::Notify) {
+            if(!t_allowNotifyFrom || !t_allowNotifyFrom->match(&source)) {
+              if(!g_quiet) {
+                g_log<<Logger::Error<<"["<<MT->getTid()<<"] dropping UDP NOTIFY from "<<source.toString()<<", address not matched by allow-notify-from"<<endl;
+              }
+
+              g_stats.sourceDisallowedNotify++;
+              return;
+            }
+          }
+
           struct timeval tv={0,0};
           HarvestTimestamp(&msgh, &tv);
           ComboAddress dest;
@@ -4606,15 +4718,72 @@ static void checkOrFixFDS()
 
 static void* recursorThread(unsigned int tid, const string& threadName);
 
-static void* pleaseSupplantACLs(std::shared_ptr<NetmaskGroup> ng)
+static void* pleaseSupplantAllowFrom(std::shared_ptr<NetmaskGroup> ng)
 {
   t_allowFrom = ng;
   return nullptr;
 }
 
+static void* pleaseSupplantAllowNotifyFrom(std::shared_ptr<NetmaskGroup> ng)
+{
+  t_allowNotifyFrom = ng;
+  return nullptr;
+}
+
+void* pleaseSupplantAllowNotifyFor(std::shared_ptr<notifyset_t> ns)
+{
+  t_allowNotifyFor = ns;
+  return nullptr;
+}
+
 int g_argc;
 char** g_argv;
 
+static std::shared_ptr<NetmaskGroup> parseACL(const std::string& aclFile, const std::string& aclSetting)
+{
+  auto result = std::make_shared<NetmaskGroup>();
+
+  if(!::arg()[aclFile].empty()) {
+    string line;
+    ifstream ifs(::arg()[aclFile].c_str());
+    if(!ifs) {
+      throw runtime_error("Could not open '"+::arg()[aclFile]+"': "+stringerror());
+    }
+
+    string::size_type pos;
+    while(getline(ifs,line)) {
+      pos=line.find('#');
+      if(pos!=string::npos)
+        line.resize(pos);
+      boost::trim(line);
+      if(line.empty())
+        continue;
+
+      result->addMask(line);
+    }
+    g_log<<Logger::Info<<"Done parsing "<<result->size()<<" "<<aclSetting<<" ranges from file '"<<::arg()[aclFile]<<"' - overriding '"<<aclSetting<<"' setting"<<endl;
+
+    return result;
+  }
+  else if(!::arg()[aclSetting].empty()) {
+    vector<string> ips;
+    stringtok(ips, ::arg()[aclSetting], ", ");
+
+    g_log<<Logger::Info<<aclSetting<<": ";
+    for(vector<string>::const_iterator i = ips.begin(); i!= ips.end(); ++i) {
+      result->addMask(*i);
+      if(i!=ips.begin())
+        g_log<<Logger::Info<<", ";
+      g_log<<Logger::Info<<*i;
+    }
+    g_log<<Logger::Info<<endl;
+
+    return result;
+  }
+
+  return nullptr;
+}
+
 void parseACLs()
 {
   static bool l_initialized;
@@ -4629,6 +4798,11 @@ void parseACLs()
     if(!::arg().preParseFile(configname.c_str(), "allow-from-file"))
       throw runtime_error("Unable to re-parse configuration file '"+configname+"'");
     ::arg().preParseFile(configname.c_str(), "allow-from", LOCAL_NETS);
+
+    if(!::arg().preParseFile(configname.c_str(), "allow-notify-from-file"))
+      throw runtime_error("Unable to re-parse configuration file '"+configname+"'");
+    ::arg().preParseFile(configname.c_str(), "allow-notify-from");
+
     ::arg().preParseFile(configname.c_str(), "include-dir");
     ::arg().preParse(g_argc, g_argv, "include-dir");
 
@@ -4638,60 +4812,38 @@ void parseACLs()
 
     for(const std::string& fn : extraConfigs) {
       if(!::arg().preParseFile(fn.c_str(), "allow-from-file", ::arg()["allow-from-file"]))
-       throw runtime_error("Unable to re-parse configuration file include '"+fn+"'");
+        throw runtime_error("Unable to re-parse configuration file include '"+fn+"'");
       if(!::arg().preParseFile(fn.c_str(), "allow-from", ::arg()["allow-from"]))
-       throw runtime_error("Unable to re-parse configuration file include '"+fn+"'");
+        throw runtime_error("Unable to re-parse configuration file include '"+fn+"'");
+
+      if(!::arg().preParseFile(fn.c_str(), "allow-notify-from-file", ::arg()["allow-notify-from-file"]))
+        throw runtime_error("Unable to re-parse configuration file include '"+fn+"'");
+      if(!::arg().preParseFile(fn.c_str(), "allow-notify-from", ::arg()["allow-notify-from"]))
+        throw runtime_error("Unable to re-parse configuration file include '"+fn+"'");
     }
 
     ::arg().preParse(g_argc, g_argv, "allow-from-file");
     ::arg().preParse(g_argc, g_argv, "allow-from");
-  }
-
-  std::shared_ptr<NetmaskGroup> oldAllowFrom = t_allowFrom;
-  std::shared_ptr<NetmaskGroup> allowFrom = std::make_shared<NetmaskGroup>();
-
-  if(!::arg()["allow-from-file"].empty()) {
-    string line;
-    ifstream ifs(::arg()["allow-from-file"].c_str());
-    if(!ifs) {
-      throw runtime_error("Could not open '"+::arg()["allow-from-file"]+"': "+stringerror());
-    }
-
-    string::size_type pos;
-    while(getline(ifs,line)) {
-      pos=line.find('#');
-      if(pos!=string::npos)
-        line.resize(pos);
-      boost::trim(line);
-      if(line.empty())
-        continue;
 
-      allowFrom->addMask(line);
-    }
-    g_log<<Logger::Warning<<"Done parsing " << allowFrom->size() <<" allow-from ranges from file '"<<::arg()["allow-from-file"]<<"' - overriding 'allow-from' setting"<<endl;
+    ::arg().preParse(g_argc, g_argv, "allow-notify-from-file");
+    ::arg().preParse(g_argc, g_argv, "allow-notify-from");
   }
-  else if(!::arg()["allow-from"].empty()) {
-    vector<string> ips;
-    stringtok(ips, ::arg()["allow-from"], ", ");
 
-    g_log<<Logger::Warning<<"Only allowing queries from: ";
-    for(vector<string>::const_iterator i = ips.begin(); i!= ips.end(); ++i) {
-      allowFrom->addMask(*i);
-      if(i!=ips.begin())
-        g_log<<Logger::Warning<<", ";
-      g_log<<Logger::Warning<<*i;
-    }
-    g_log<<Logger::Warning<<endl;
-  }
-  else {
+  auto allowFrom = parseACL("allow-from-file", "allow-from");
+
+  if(allowFrom->size() == 0) {
     if(::arg()["local-address"]!="127.0.0.1" && ::arg().asNum("local-port")==53)
       g_log<<Logger::Warning<<"WARNING: Allowing queries from all IP addresses - this can be a security risk!"<<endl;
     allowFrom = nullptr;
   }
 
   g_initialAllowFrom = allowFrom;
-  broadcastFunction([=]{ return pleaseSupplantACLs(allowFrom); });
-  oldAllowFrom = nullptr;
+  broadcastFunction([=]{ return pleaseSupplantAllowFrom(allowFrom); });
+
+  auto allowNotifyFrom = parseACL("allow-notify-from-file", "allow-notify-from");
+
+  g_initialAllowNotifyFrom = allowNotifyFrom;
+  broadcastFunction([=]{ return pleaseSupplantAllowNotifyFrom(allowNotifyFrom); });
 
   l_initialized = true;
 }
@@ -5129,7 +5281,7 @@ static int serviceMain(int argc, char*argv[])
 
   g_networkTimeoutMsec = ::arg().asNum("network-timeout");
 
-  g_initialDomainMap = parseAuthAndForwards();
+  std::tie(g_initialDomainMap, g_initialAllowNotifyFor) = parseZoneConfiguration();
 
   g_latencyStatSize=::arg().asNum("latency-statistic-size");
 
@@ -5551,6 +5703,8 @@ try
   SyncRes tmp(g_now); // make sure it allocates tsstorage before we do anything, like primeHints or so..
   SyncRes::setDomainMap(g_initialDomainMap);
   t_allowFrom = g_initialAllowFrom;
+  t_allowNotifyFrom = g_initialAllowNotifyFrom;
+  t_allowNotifyFor = g_initialAllowNotifyFor;
   t_udpclientsocks = std::make_unique<UDPClientSocks>();
   t_tcpClientCounts = std::make_unique<tcpClientCounts_t>();
 
@@ -5914,6 +6068,10 @@ int main(int argc, char **argv)
     ::arg().set("version-string", "string reported on version.pdns or version.bind")=fullVersionString();
     ::arg().set("allow-from", "If set, only allow these comma separated netmasks to recurse")=LOCAL_NETS;
     ::arg().set("allow-from-file", "If set, load allowed netmasks from this file")="";
+    ::arg().set("allow-notify-for", "If set, NOTIFY requests for these zones will be allowed")="";
+    ::arg().set("allow-notify-for-file", "If set, load NOTIFY-allowed zones from this file")="";
+    ::arg().set("allow-notify-from", "If set, NOTIFY requests from these comma separated netmasks will be allowed")="";
+    ::arg().set("allow-notify-from-file", "If set, load NOTIFY-allowed netmasks from this file")="";
     ::arg().set("entropy-source", "If set, read entropy from this file")="/dev/urandom";
     ::arg().set("dont-query", "If set, do not query these netmasks for DNS data")=DONT_QUERY;
     ::arg().set("max-tcp-per-client", "If set, maximum number of TCP sessions per client (IP address)")="0";
index 3bb7783a3bebf796efa109204a5a8ca70bfc827e..d6867d86a47278e9cedaa38759ce877e83b54951 100644 (file)
@@ -143,6 +143,8 @@ static const oid udp6NoportErrorsOID[] = {RECURSOR_STATS_OID, 121};
 static const oid udp6InErrorsOID[] = {RECURSOR_STATS_OID, 122};
 static const oid udp6InCsumErrorsOID[] = {RECURSOR_STATS_OID, 123};
 #endif /* __linux__ */
+static const oid sourceDisallowedNotifyOID[] = {RECURSOR_STATS_OID, 124};
+static const oid zoneDisallowedNotifyOID[] = {RECURSOR_STATS_OID, 125};
 
 static std::unordered_map<oid, std::string> s_statsMap;
 
@@ -267,6 +269,8 @@ RecursorSNMPAgent::RecursorSNMPAgent(const std::string& name, const std::string&
   registerCounter64Stat("noerror-answers", noerrorAnswersOID, OID_LENGTH(noerrorAnswersOID));
   registerCounter64Stat("unauthorized-udp", unauthorizedUdpOID, OID_LENGTH(unauthorizedUdpOID));
   registerCounter64Stat("unauthorized-tcp", unauthorizedTcpOID, OID_LENGTH(unauthorizedTcpOID));
+  registerCounter64Stat("source-disallowed-notify", sourceDisallowedNotifyOID, OID_LENGTH(sourceDisallowedNotifyOID));
+  registerCounter64Stat("zone-disallowed-notify", zoneDisallowedNotifyOID, OID_LENGTH(zoneDisallowedNotifyOID));
   registerCounter64Stat("tcp-client-overflow", tcpClientOverflowOID, OID_LENGTH(tcpClientOverflowOID));
   registerCounter64Stat("client-parse-errors", clientParseErrorsOID, OID_LENGTH(clientParseErrorsOID));
   registerCounter64Stat("server-parse-errors", serverParseErrorsOID, OID_LENGTH(serverParseErrorsOID));
index 3d74674d9283438652e7a8ac4541a9ed65537bec..c87c4cc56b5027d6358c0c869416c3d7e623962b 100644 (file)
@@ -1195,6 +1195,8 @@ static void registerAllStats1()
 
   addGetStat("unauthorized-udp", &g_stats.unauthorizedUDP);
   addGetStat("unauthorized-tcp", &g_stats.unauthorizedTCP);
+  addGetStat("source-disallowed-notify", &g_stats.sourceDisallowedNotify);
+  addGetStat("zone-disallowed-notify", &g_stats.zoneDisallowedNotify);
   addGetStat("tcp-client-overflow", &g_stats.tcpClientOverflow);
 
   addGetStat("client-parse-errors", &g_stats.clientParseError);
@@ -2108,7 +2110,7 @@ RecursorControlChannel::Answer RecursorControlParser::getAnswer(int s, const str
       g_log << Logger::Error << "Unable to reload zones and forwards when chroot()'ed, requested via control channel" << endl;
       return {1, "Unable to reload zones and forwards when chroot()'ed, please restart\n"};
     }
-    return {0, reloadAuthAndForwards()};
+    return {0, reloadZoneConfiguration()};
   }
   if (cmd == "set-ecs-minimum-ttl") {
     return {0, setMinimumECSTTL(begin, end)};
index d9fea57ec79314bf82f736d97c60fa719229a7c9..83359c73190421b445492a84fa2a012d81ef995e 100644 (file)
@@ -42,6 +42,9 @@ rec MODULE-IDENTITY
     REVISION "202110270000Z"
     DESCRIPTION "Added more UDP errors metric."
 
+    REVISION "202111090000Z"
+    DESCRIPTION "Added NOTIFY-related metrics."
+
     ::= { powerdns 2 }
 
 powerdns               OBJECT IDENTIFIER ::= { enterprises 43315 }
@@ -1032,6 +1035,21 @@ udp6InCsumErrors OBJECT-TYPE
         "Number of UDP6 in checksum errors (Linux only)"
     ::= { stats 123 }
 
+sourceDisallowedNotify OBJECT-TYPE
+    SYNTAX Counter64
+    MAX-ACCESS read-only
+    STATUS current
+    DESCRIPTION
+        "Number of NOTIFY operations not authorized by allow-notify-from"
+    ::= { stats 124 }
+
+zoneDisallowedNotify OBJECT-TYPE
+    SYNTAX Counter64
+    MAX-ACCESS read-only
+    STATUS current
+    DESCRIPTION
+        "Number of NOTIFY operations not allowed by allow-notify-for"
+    ::= { stats 125 }
 ---
 --- Traps / Notifications
 ---
index 6fe6d88333be0db2421a8ede1b914fb7143933e6..713402d57759323b58de2552cfd31a55e50b8335 100644 (file)
@@ -5,7 +5,7 @@
   Change a single setting
 
   .. note::
-    Only :ref:`setting-allow-from` can be set.
+    Only :ref:`setting-allow-from` and :ref:`setting-allow-notify-from` can be set.
 
   :param server_id: The name of the server
   :param config_setting_name: The name of the setting to change
index 8491485d8fbee16d53a6bb6d5b4d5fd2078094c7..5223f78c86590dfc58c830a7859c827aeb11a55e 100644 (file)
@@ -763,6 +763,14 @@ unauthorized-udp
 ^^^^^^^^^^^^^^^^
 number of UDP questions denied because of   allow-from restrictions
 
+source-disallowed-notify
+^^^^^^^^^^^^^^^^^^^^^^^^
+number of NOTIFY operations denied because of allow-notify-from restrictions
+
+zone-disallowed-notify
+^^^^^^^^^^^^^^^^^^^^^^
+number of NOTIFY operations denied because of allow-notify-for restrictions
+
 unexpected-packets
 ^^^^^^^^^^^^^^^^^^
 number of answers from remote servers that   were unexpected (might point to spoofing)
index d14c27dafa88aac8a13384cedac84cdaa7ee2342..156ee9930834c5f49673b5001f2e046bc02a2f68 100644 (file)
@@ -58,6 +58,69 @@ Note that specifying an IP address without a netmask uses an implicit netmask of
 Like `allow-from`_, except reading from file.
 Overrides the `allow-from`_ setting. To use this feature, supply one netmask per line, with optional comments preceded by a "#".
 
+.. _setting-allow-notify-for:
+
+``allow-notify-for``
+---------------------
+.. versionadded:: 4.6.0
+
+-  Comma separated list of domain-names
+-  Default: (empty)
+
+Domain names specified in this list are used to permit incoming
+NOTIFY operations to wipe any cache entries that match the domain
+name. If this list is empty, all NOTIFY operations will be ignored.
+
+.. _setting-allow-notify-for-file:
+
+``allow-notify-for-file``
+-------------------------
+.. versionadded:: 4.6.0
+
+-  Path
+
+Like `allow-notify-for`_, except reading from file. To use this
+feature, supply one domain name per line, with optional comments
+preceded by a "#".
+
+.. _setting-allow-notify-from:
+
+``allow-notify-from``
+---------------------
+.. versionadded:: 4.6.0
+
+-  IP addresses or netmasks, separated by commas
+-  Default: unset
+
+Netmasks (both IPv4 and IPv6) that are allowed to issue NOTIFY operations
+to the server.  NOTIFY operations from IP addresses not listed here are
+ignored and do not get an answer.
+
+When the Proxy Protocol is enabled (see `proxy-protocol-from`_), the
+recursor will check the address of the client IP advertised in the
+Proxy Protocol header instead of the one of the proxy.
+
+Note that specifying an IP address without a netmask uses an implicit
+netmask of /32 or /128.
+
+NOTIFY operations received from a client listed in one of these netmasks
+will be accepted and used to wipe any cache entries whose zones match
+the zone specified in the NOTIFY operation, but only if that zone (or
+one of its parents) is included in `allow-notify-for`_,
+`allow-notify-for-file`_, or `forward-zones-file_` with a '^' prefix.
+
+.. _setting-allow-notify-from-file:
+
+``allow-notify-from-file``
+--------------------------
+.. versionadded:: 4.6.0
+
+-  Path
+
+Like `allow-notify-from`_, except reading from file. To use this
+feature, supply one netmask per line, with optional comments preceded
+by a "#".
+
 .. _setting-any-to-tcp:
 
 ``any-to-tcp``
@@ -785,8 +848,9 @@ Same as `forward-zones`_, parsed from a file. Only 1 zone is allowed per line, s
 
     example.org=203.0.113.210, 192.0.2.4:5300
 
-Zones prefixed with a '+' are forwarded with the recursion-desired bit set, for which see `forward-zones-recurse`_.
-Default behaviour without '+' is as with `forward-zones`_.
+Zones prefixed with a '+' are treated as with
+`forward-zones-recurse`_.  Default behaviour without '+' is as with
+`forward-zones`_.
 
 .. versionchanged:: 4.0.0
 
@@ -794,6 +858,11 @@ Default behaviour without '+' is as with `forward-zones`_.
 
 The DNSSEC notes from `forward-zones`_ apply here as well.
 
+.. versionchanged:: 4.6.0
+
+Zones prefixed with a '^' are added to the `allow-notify-for`_
+list. Both prefix characters can be used if desired, in any order.
+
 .. _setting-forward-zones-recurse:
 
 ``forward-zones-recurse``
index f5d417e3c8d73a17e33509b47c1d4174beef2d34..10418a6b2d940d244e845d5ffa293b36ad7132f8 100644 (file)
@@ -295,7 +295,7 @@ static void* pleaseUseNewSDomainsMap(std::shared_ptr<SyncRes::domainmap_t> newma
   return 0;
 }
 
-string reloadAuthAndForwards()
+string reloadZoneConfiguration()
 {
   std::shared_ptr<SyncRes::domainmap_t> original = SyncRes::getDomainMap();
 
@@ -313,6 +313,8 @@ string reloadAuthAndForwards()
     ::arg().preParseFile(configname.c_str(), "forward-zones-file");
     ::arg().preParseFile(configname.c_str(), "forward-zones-recurse");
     ::arg().preParseFile(configname.c_str(), "auth-zones");
+    ::arg().preParseFile(configname.c_str(), "allow-notify-for");
+    ::arg().preParseFile(configname.c_str(), "allow-notify-for-file");
     ::arg().preParseFile(configname.c_str(), "export-etc-hosts", "off");
     ::arg().preParseFile(configname.c_str(), "serve-rfc1918");
     ::arg().preParseFile(configname.c_str(), "include-dir");
@@ -328,6 +330,8 @@ string reloadAuthAndForwards()
       ::arg().preParseFile(fn.c_str(), "forward-zones-file", ::arg()["forward-zones-file"]);
       ::arg().preParseFile(fn.c_str(), "forward-zones-recurse", ::arg()["forward-zones-recurse"]);
       ::arg().preParseFile(fn.c_str(), "auth-zones", ::arg()["auth-zones"]);
+      ::arg().preParseFile(fn.c_str(), "allow-notify-for", ::arg()["allow-notify-for"]);
+      ::arg().preParseFile(fn.c_str(), "allow-notify-for-file", ::arg()["allow-notify-for-file"]);
       ::arg().preParseFile(fn.c_str(), "export-etc-hosts", ::arg()["export-etc-hosts"]);
       ::arg().preParseFile(fn.c_str(), "serve-rfc1918", ::arg()["serve-rfc1918"]);
     }
@@ -336,10 +340,12 @@ string reloadAuthAndForwards()
     ::arg().preParse(g_argc, g_argv, "forward-zones-file");
     ::arg().preParse(g_argc, g_argv, "forward-zones-recurse");
     ::arg().preParse(g_argc, g_argv, "auth-zones");
+    ::arg().preParse(g_argc, g_argv, "allow-notify-for");
+    ::arg().preParse(g_argc, g_argv, "allow-notify-for-file");
     ::arg().preParse(g_argc, g_argv, "export-etc-hosts");
     ::arg().preParse(g_argc, g_argv, "serve-rfc1918");
 
-    std::shared_ptr<SyncRes::domainmap_t> newDomainMap = parseAuthAndForwards();
+    auto [newDomainMap, newNotifySet] = parseZoneConfiguration();
 
     // purge both original and new names
     std::set<DNSName> oldAndNewDomains;
@@ -357,7 +363,11 @@ string reloadAuthAndForwards()
       wipeCaches(i, true, 0xffff);
     }
 
-    broadcastFunction([=] { return pleaseUseNewSDomainsMap(newDomainMap); });
+    // these explicitly-named captures should not be necessary, as lambda
+    // capture of tuple-like structured bindings is permitted, but some
+    // compilers still don't allow it
+    broadcastFunction([dm = newDomainMap] { return pleaseUseNewSDomainsMap(dm); });
+    broadcastFunction([ns = newNotifySet] { return pleaseSupplantAllowNotifyFor(ns); });
     return "ok\n";
   }
   catch (std::exception& e) {
@@ -372,12 +382,13 @@ string reloadAuthAndForwards()
   return "reloading failed, see log\n";
 }
 
-std::shared_ptr<SyncRes::domainmap_t> parseAuthAndForwards()
+std::tuple<std::shared_ptr<SyncRes::domainmap_t>, std::shared_ptr<notifyset_t>> parseZoneConfiguration()
 {
   TXTRecordContent::report();
   OPTRecordContent::report();
 
   auto newMap = std::make_shared<SyncRes::domainmap_t>();
+  auto newSet = std::make_shared<notifyset_t>();
 
   typedef vector<string> parts_t;
   parts_t parts;
@@ -439,7 +450,6 @@ std::shared_ptr<SyncRes::domainmap_t> parseAuthAndForwards()
 
   if (!::arg()["forward-zones-file"].empty()) {
     g_log << Logger::Warning << "Reading zone forwarding information from '" << ::arg()["forward-zones-file"] << "'" << endl;
-    SyncRes::AuthDomain ad;
     auto fp = std::unique_ptr<FILE, int (*)(FILE*)>(fopen(::arg()["forward-zones-file"].c_str(), "r"), fclose);
     if (!fp) {
       throw PDNSException("Error opening forward-zones-file '" + ::arg()["forward-zones-file"] + "': " + stringerror());
@@ -449,6 +459,7 @@ std::shared_ptr<SyncRes::domainmap_t> parseAuthAndForwards()
     int linenum = 0;
     uint64_t before = newMap->size();
     while (linenum++, stringfgets(fp.get(), line)) {
+      SyncRes::AuthDomain ad;
       boost::trim(line);
       if (line[0] == '#') // Comment line, skip to the next line
         continue;
@@ -457,15 +468,27 @@ std::shared_ptr<SyncRes::domainmap_t> parseAuthAndForwards()
       instructions = splitField(instructions, '#').first; // Remove EOL comments
       boost::trim(domain);
       boost::trim(instructions);
-      if (domain.empty() && instructions.empty()) { // empty line
-        continue;
+      if (domain.empty()) {
+        if (instructions.empty()) { // empty line
+          continue;
+        }
+        throw PDNSException("Error parsing line " + std::to_string(linenum) + " of " + ::arg()["forward-zones-file"]);
       }
-      if (boost::starts_with(domain, "+")) {
-        domain = domain.c_str() + 1;
-        ad.d_rdForward = true;
+
+      bool allowNotifyFor = false;
+
+      for (; !domain.empty(); domain.erase(0, 1)) {
+        switch (domain[0]) {
+        case '+':
+          ad.d_rdForward = true;
+          continue;
+        case '^':
+          allowNotifyFor = true;
+          continue;
+        }
+        break;
       }
-      else
-        ad.d_rdForward = false;
+
       if (domain.empty()) {
         throw PDNSException("Error parsing line " + std::to_string(linenum) + " of " + ::arg()["forward-zones-file"]);
       }
@@ -479,6 +502,9 @@ std::shared_ptr<SyncRes::domainmap_t> parseAuthAndForwards()
 
       ad.d_name = DNSName(domain);
       (*newMap)[ad.d_name] = ad;
+      if (allowNotifyFor) {
+        newSet->insert(ad.d_name);
+      }
     }
     g_log << Logger::Warning << "Done parsing " << newMap->size() - before << " forwarding instructions from file '" << ::arg()["forward-zones-file"] << "'" << endl;
   }
@@ -520,6 +546,7 @@ std::shared_ptr<SyncRes::domainmap_t> parseAuthAndForwards()
       }
     }
   }
+
   if (::arg().mustDo("serve-rfc1918")) {
     g_log << Logger::Warning << "Inserting rfc 1918 private space zones" << endl;
     parts.clear();
@@ -535,5 +562,30 @@ std::shared_ptr<SyncRes::domainmap_t> parseAuthAndForwards()
       makeIPToNamesZone(newMap, parts);
     }
   }
-  return newMap;
+
+  parts.clear();
+  stringtok(parts, ::arg()["allow-notify-for"], " ,\t\n\r");
+  for (parts_t::const_iterator iter = parts.begin(); iter != parts.end(); ++iter) {
+    newSet->insert(DNSName(*iter));
+  }
+
+  if (auto anff = ::arg()["allow-notify-for-file"]; !anff.empty()) {
+    g_log << Logger::Warning << "Reading NOTIFY-allowed zones from '" << anff << "'" << endl;
+    auto fp = std::unique_ptr<FILE, int (*)(FILE*)>(fopen(anff.c_str(), "r"), fclose);
+    if (!fp) {
+      throw PDNSException("Error opening allow-notify-for-file '" + anff + "': " + stringerror());
+    }
+
+    string line;
+    uint64_t before = newSet->size();
+    while (stringfgets(fp.get(), line)) {
+      boost::trim(line);
+      if (line[0] == '#') // Comment line, skip to the next line
+        continue;
+      newSet->insert(DNSName(line));
+    }
+    g_log << Logger::Warning << "Done parsing " << newSet->size() - before << " NOTIFY-allowed zones from file '" << anff << "'" << endl;
+  }
+
+  return {newMap, newSet};
 }
index 0506fdabf8b63aad8b9c430fd9db19146e417779..5d51b4e5bbb470126bf388620001b0e889966e60 100644 (file)
@@ -1047,6 +1047,8 @@ struct RecursorStats
   pdns::stat_t tcpqcounter;
   pdns::stat_t unauthorizedUDP;  // when this is increased, qcounter isn't
   pdns::stat_t unauthorizedTCP;  // when this is increased, qcounter isn't
+  pdns::stat_t sourceDisallowedNotify;  // when this is increased, qcounter is also
+  pdns::stat_t zoneDisallowedNotify;  // when this is increased, qcounter is also
   pdns::stat_t policyDrops;
   pdns::stat_t tcpClientOverflow;
   pdns::stat_t clientParseError;
@@ -1155,6 +1157,7 @@ extern thread_local std::unique_ptr<addrringbuf_t> t_servfailremotes, t_largeans
 
 extern thread_local std::unique_ptr<boost::circular_buffer<pair<DNSName,uint16_t> > > t_queryring, t_servfailqueryring, t_bogusqueryring;
 extern thread_local std::shared_ptr<NetmaskGroup> t_allowFrom;
+extern thread_local std::shared_ptr<NetmaskGroup> t_allowNotifyFrom;
 string doTraceRegex(vector<string>::const_iterator begin, vector<string>::const_iterator end);
 void parseACLs();
 extern RecursorStats g_stats;
@@ -1165,7 +1168,7 @@ extern std::atomic<uint32_t> g_maxCacheEntries, g_maxPacketCacheEntries;
 extern bool g_lowercaseOutgoing;
 
 
-std::string reloadAuthAndForwards();
+std::string reloadZoneConfiguration();
 typedef boost::function<void*(void)> pipefunc_t;
 void broadcastFunction(const pipefunc_t& func);
 void distributeAsyncFunction(const std::string& question, const pipefunc_t& func);
@@ -1178,7 +1181,10 @@ int getFakePTRRecords(const DNSName& qname, vector<DNSRecord>& ret);
 
 template<class T> T broadcastAccFunction(const boost::function<T*()>& func);
 
-std::shared_ptr<SyncRes::domainmap_t> parseAuthAndForwards();
+typedef std::unordered_set<DNSName> notifyset_t;
+std::tuple<std::shared_ptr<SyncRes::domainmap_t>, std::shared_ptr<notifyset_t>> parseZoneConfiguration();
+void* pleaseSupplantAllowNotifyFor(std::shared_ptr<notifyset_t> ns);
+
 uint64_t* pleaseGetNsSpeedsSize();
 uint64_t* pleaseGetFailedServersSize();
 uint64_t* pleaseGetEDNSStatusesSize();
index 3c2ad0c083d1b3cc818475b59cf3afc02643ff65..84feae92864017eb44fb396dfd4e6de5d6f253ba 100644 (file)
@@ -79,7 +79,7 @@ static void apiWriteConfigFile(const string& filebasename, const string& content
   ofconf.close();
 }
 
-static void apiServerConfigAllowFrom(HttpRequest* req, HttpResponse* resp)
+static void apiServerConfigACL(const std::string& aclType, HttpRequest* req, HttpResponse* resp)
 {
   if (req->method == "PUT") {
     Json document = req->json();
@@ -101,14 +101,14 @@ static void apiServerConfigAllowFrom(HttpRequest* req, HttpResponse* resp)
 
     ostringstream ss;
 
-    // Clear allow-from-file if set, so our changes take effect
-    ss << "allow-from-file=" << endl;
+    // Clear <foo>-from-file if set, so our changes take effect
+    ss << aclType << "-file=" << endl;
 
-    // Clear allow-from, and provide a "parent" value
-    ss << "allow-from=" << endl;
-    ss << "allow-from+=" << nmg.toString() << endl;
+    // Clear ACL setting, and provide a "parent" value
+    ss << aclType << "=" << endl;
+    ss << aclType << "+=" << nmg.toString() << endl;
 
-    apiWriteConfigFile("allow-from", ss.str());
+    apiWriteConfigFile(aclType, ss.str());
 
     parseACLs();
 
@@ -120,14 +120,29 @@ static void apiServerConfigAllowFrom(HttpRequest* req, HttpResponse* resp)
 
   // Return currently configured ACLs
   vector<string> entries;
-  t_allowFrom->toStringVector(&entries);
+  if (aclType == "allow-from") {
+    t_allowFrom->toStringVector(&entries);
+  }
+  else if (aclType == "allow-notify-from") {
+    t_allowNotifyFrom->toStringVector(&entries);
+  }
 
   resp->setJsonBody(Json::object{
-    {"name", "allow-from"},
+    {"name", aclType},
     {"value", entries},
   });
 }
 
+static void apiServerConfigAllowFrom(HttpRequest* req, HttpResponse* resp)
+{
+  apiServerConfigACL("allow-from", req, resp);
+}
+
+static void apiServerConfigAllowNotifyFrom(HttpRequest* req, HttpResponse* resp)
+{
+  apiServerConfigACL("allow-notify-from", req, resp);
+}
+
 static void fillZone(const DNSName& zonename, HttpResponse* resp)
 {
   auto iter = SyncRes::t_sstorage.domainmap->find(zonename);
@@ -277,7 +292,7 @@ static void apiServerZones(HttpRequest* req, HttpResponse* resp)
       throw ApiException("Zone already exists");
 
     doCreateZone(document);
-    reloadAuthAndForwards();
+    reloadZoneConfiguration();
     fillZone(zonename, resp);
     resp->status = 201;
     return;
@@ -319,7 +334,7 @@ static void apiServerZoneDetail(HttpRequest* req, HttpResponse* resp)
 
     doDeleteZone(zonename);
     doCreateZone(document);
-    reloadAuthAndForwards();
+    reloadZoneConfiguration();
     resp->body = "";
     resp->status = 204; // No Content, but indicate success
   }
@@ -328,7 +343,7 @@ static void apiServerZoneDetail(HttpRequest* req, HttpResponse* resp)
       throw ApiException("Deleting domain failed");
     }
 
-    reloadAuthAndForwards();
+    reloadZoneConfiguration();
     // empty body on success
     resp->body = "";
     resp->status = 204; // No Content: declare that the zone is gone now
@@ -609,6 +624,9 @@ const std::map<std::string, MetricDefinition> MetricDefinitionStorage::d_metrics
    MetricDefinition(PrometheusMetricType::multicounter,
                     "Number of milliseconds spent in thread n")},
 
+  {"zone-disallowed-notify",
+   MetricDefinition(PrometheusMetricType::counter,
+                    "Number of NOTIFY operations denied because of allow-notify-for restrictions")},
   {"dnssec-authentic-data-queries",
    MetricDefinition(PrometheusMetricType::counter,
                     "Number of queries received with the AD bit set")},
@@ -835,6 +853,9 @@ const std::map<std::string, MetricDefinition> MetricDefinitionStorage::d_metrics
   {"unauthorized-udp",
    MetricDefinition(PrometheusMetricType::counter,
                     "Number of UDP questions denied because of allow-from restrictions")},
+  {"source-disallowed-notify",
+   MetricDefinition(PrometheusMetricType::counter,
+                    "Number of NOTIFY operations denied because of allow-notify-from restrictions")},
   {"unexpected-packets",
    MetricDefinition(PrometheusMetricType::counter,
                     "Number of answers from remote servers that were unexpected (might point to spoofing)")},
@@ -1169,6 +1190,7 @@ RecursorWebServer::RecursorWebServer(FDMultiplexer* fdm)
     "/jsonstat", [this](HttpRequest* req, HttpResponse* resp) { jsonstat(req, resp); }, true);
   d_ws->registerApiHandler("/api/v1/servers/localhost/cache/flush", apiServerCacheFlush);
   d_ws->registerApiHandler("/api/v1/servers/localhost/config/allow-from", apiServerConfigAllowFrom);
+  d_ws->registerApiHandler("/api/v1/servers/localhost/config/allow-notify-from", &apiServerConfigAllowNotifyFrom);
   d_ws->registerApiHandler("/api/v1/servers/localhost/config", apiServerConfig);
   d_ws->registerApiHandler("/api/v1/servers/localhost/rpzstatistics", apiServerRPZStats);
   d_ws->registerApiHandler("/api/v1/servers/localhost/search-data", apiServerSearchData);
index 956047e993877708e99c01bcc72f6691940e6cb4..bf362eb4d6a9167c92a953ef136015c957f8993e 100755 (executable)
@@ -87,6 +87,13 @@ ACL_LIST_TPL = """
 ::1
 """
 
+ACL_NOTIFY_LIST_TPL = """
+# Generated by runtests.py
+# local host
+127.0.0.1
+::1
+"""
+
 REC_EXAMPLE_COM_CONF_TPL = """
 # Generated by runtests.py
 auth-zones+=example.com=../regression-tests/zones/example.com
@@ -98,6 +105,7 @@ auth-zones=
 forward-zones=
 forward-zones-recurse=
 allow-from-file=acl.list
+allow-notify-from-file=acl-notify.list
 api-config-dir=%(conf_dir)s
 include-dir=%(conf_dir)s
 """
@@ -214,6 +222,8 @@ else:
     ensure_empty_dir(conf_dir)
     with open('acl.list', 'w') as acl_list:
         acl_list.write(ACL_LIST_TPL)
+    with open('acl-notify.list', 'w') as acl_notify_list:
+        acl_notify_list.write(ACL_NOTIFY_LIST_TPL)
     with open('recursor.conf', 'w') as recursor_conf:
         recursor_conf.write(REC_CONF_TPL % locals())
     with open(conf_dir+'/example.com..conf', 'w') as conf_file:
index d8cba15138bf7aea4362c43f2aeaafd84a5d946c..774d366ad089249caf43bfa9d26c3e23c0a15797 100644 (file)
@@ -4,7 +4,7 @@ from test_helper import ApiTestCase, is_recursor
 
 
 @unittest.skipIf(not is_recursor(), "Only applicable to recursors")
-class RecursorConfig(ApiTestCase):
+class RecursorAllowFromConfig(ApiTestCase):
 
     def test_config_allow_from_get(self):
         r = self.session.get(self.url("/api/v1/servers/localhost/config/allow-from"))
@@ -32,3 +32,34 @@ class RecursorConfig(ApiTestCase):
         self.assertEqual(r.status_code, 422)
         data = r.json()
         self.assertIn('Unable to convert', data['error'])
+
+
+@unittest.skipIf(not is_recursor(), "Only applicable to recursors")
+class RecursorAllowNotifyFromConfig(ApiTestCase):
+
+    def test_config_allow_notify_from_get(self):
+        r = self.session.get(self.url("/api/v1/servers/localhost/config/allow-notify-from"))
+        self.assert_success_json(r)
+
+    def test_config_allow_notify_from_replace(self):
+        payload = {'value': ["127.0.0.1"]}
+        r = self.session.put(
+            self.url("/api/v1/servers/localhost/config/allow-notify-from"),
+            data=json.dumps(payload),
+            headers={'content-type': 'application/json'})
+        self.assert_success_json(r)
+        data = r.json()
+        self.assertIn("value", data)
+        self.assertEqual(len(data["value"]), 1)
+        self.assertEqual("127.0.0.1/32", data["value"][0])
+
+    def test_config_allow_notify_from_replace_error(self):
+        """Test the error case, should return 422."""
+        payload = {'value': ["abcdefgh"]}
+        r = self.session.put(
+            self.url("/api/v1/servers/localhost/config/allow-notify-from"),
+            data=json.dumps(payload),
+            headers={'content-type': 'application/json'})
+        self.assertEqual(r.status_code, 422)
+        data = r.json()
+        self.assertIn('Unable to convert', data['error'])