]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
auth: incoming PROXY support for:
authorPeter van Dijk <peter@7bits.nl>
Tue, 24 Aug 2021 08:44:44 +0000 (10:44 +0200)
committerPeter van Dijk <peter.van.dijk@powerdns.com>
Thu, 16 Sep 2021 07:34:32 +0000 (09:34 +0200)
* AXFR ACLs
* NOTIFY sources
* getting the remote address in LUA records

15 files changed:
modules/remotebackend/Makefile.am
pdns/Makefile.am
pdns/common_startup.cc
pdns/common_startup.hh
pdns/dnspacket.cc
pdns/dnspacket.hh
pdns/lua-record.cc
pdns/nameserver.cc
pdns/packethandler.cc
pdns/proxy-protocol.hh
pdns/tcpreceiver.cc
pdns/tcpreceiver.hh
pdns/test-nameserver_cc.cc
regression-tests.auth-py/proxyprotocol.py [new symlink]
regression-tests.auth-py/test_ProxyProtocol.py [new file with mode: 0644]

index c544005905ea27d42a353ac42ff4c2398aa9daa2..f1aaa95e0c52e2d2a8729f2bf2f6e63a09626b18 100644 (file)
@@ -4,6 +4,10 @@ AM_CPPFLAGS += \
        $(LIBCRYPTO_CFLAGS) \
        $(LIBZMQ_CFLAGS)
 
+if LUA
+AM_CPPFLAGS +=$(LUA_CFLAGS)
+endif
+
 AM_LDFLAGS = $(THREADFLAGS)
 
 JSON11_LIBS = $(top_builddir)/ext/json11/libjson11.la
index a17aa95e0aeae8a5c3cdfce2fdeb33ebb0063e9a..20113ebeeb670508f29858dac03bc45135ddd99d 100644 (file)
@@ -239,6 +239,7 @@ pdns_server_SOURCES = \
        packetcache.hh \
        packethandler.cc packethandler.hh \
        pdnsexception.hh \
+       proxy-protocol.cc proxy-protocol.hh \
        qtype.cc qtype.hh \
        query-local-address.hh query-local-address.cc \
        rcpgenerator.cc \
index afc9b0b685d5ec83c574d42d523fcdf3f0c34e19..e92fc3d3d39f84ec763abe368a0ff9148f5d4c81 100644 (file)
@@ -64,6 +64,8 @@ double avg_latency{0.0};
 unique_ptr<TCPNameserver> TN;
 static vector<DNSDistributor*> g_distributors;
 vector<std::shared_ptr<UDPNameserver> > g_udpReceivers;
+NetmaskGroup g_proxyProtocolACL;
+size_t g_proxyProtocolMaximumSize;
 
 ArgvMap &arg()
 {
@@ -93,6 +95,8 @@ void declareArguments()
   ::arg().setSwitch("dnsupdate","Enable/Disable DNS update (RFC2136) support. Default is no.")="no";
   ::arg().setSwitch("write-pid","Write a PID file")="yes";
   ::arg().set("allow-dnsupdate-from","A global setting to allow DNS updates from these IP ranges.")="127.0.0.0/8,::1";
+  ::arg().set("proxy-protocol-from","A Proxy Protocol header is only allowed from these subnets, and is mandatory then too.")="";
+  ::arg().set("proxy-protocol-maximum-size", "The maximum size of a proxy protocol payload, including the TLV values")="512";
   ::arg().setSwitch("send-signed-notify", "Send TSIG secured NOTIFY if TSIG key is configured for a zone") = "yes";
   ::arg().set("allow-unsigned-notify", "Allow unsigned notifications for TSIG secured zones") = "yes"; //FIXME: change to 'no' later
   ::arg().set("allow-unsigned-supermaster", "Allow supermasters to create zones without TSIG signed NOTIFY")="yes";
@@ -435,7 +439,6 @@ try
   bool logDNSQueries = ::arg().mustDo("log-dns-queries");
   shared_ptr<UDPNameserver> NS;
   std::string buffer;
-  buffer.resize(DNSPacket::s_udpTruncationThreshold);
 
   // If we have SO_REUSEPORT then create a new port for all receiver threads
   // other than the first one.
@@ -449,6 +452,13 @@ try
   }
 
   for(;;) {
+    if (g_proxyProtocolACL.empty()) {
+      buffer.resize(DNSPacket::s_udpTruncationThreshold);
+    }
+    else {
+      buffer.resize(DNSPacket::s_udpTruncationThreshold + g_proxyProtocolMaximumSize);
+    }
+
     if(!NS->receive(question, buffer)) { // receive a packet         inline
       continue;                    // packet was broken, try again
     }
@@ -469,12 +479,7 @@ try
     S.ringAccount("queries", question.qdomain, question.qtype);
     S.ringAccount("remotes", question.d_remote);
     if(logDNSQueries) {
-      string remote;
-      if(question.hasEDNSSubnet()) 
-        remote = question.getRemote().toString() + "<-" + question.getRealRemote().toString();
-      else
-        remote = question.getRemote().toString();
-      g_log << Logger::Notice<<"Remote "<< remote <<" wants '" << question.qdomain<<"|"<<question.qtype.toString() << 
+      g_log << Logger::Notice<<"Remote "<< question.getRemoteString() <<" wants '" << question.qdomain<<"|"<<question.qtype.toString() <<
         "', do = " <<question.d_dnssecOk <<", bufsize = "<< question.getMaxReplyLen();
       if(question.d_ednsRawPacketSizeLimit > 0 && question.getMaxReplyLen() != (unsigned int)question.d_ednsRawPacketSizeLimit)
         g_log<<" ("<<question.d_ednsRawPacketSizeLimit<<")";
@@ -563,6 +568,9 @@ void mainthread()
    DNSPacket::s_doEDNSSubnetProcessing = ::arg().mustDo("edns-subnet-processing");
    PacketHandler::s_SVCAutohints = ::arg().mustDo("svc-autohints");
 
+   g_proxyProtocolACL.toMasks(::arg()["proxy-protocol-from"]);
+   g_proxyProtocolMaximumSize = ::arg().asNum("proxy-protocol-maximum-size");
+
    PC.setTTL(::arg().asNum("cache-ttl"));
    PC.setMaxEntries(::arg().asNum("max-packet-cache-entries"));
    QC.setMaxEntries(::arg().asNum("max-cache-entries"));
index c2e46ad6001386ce2565370e0fd8b992cbd88991..849bf243579f6960d9f006c4acd31b49ae1787db 100644 (file)
@@ -53,6 +53,8 @@ extern int isGuarded( char ** );
 void carbonDumpThread();
 extern bool g_anyToTcp;
 extern bool g_8bitDNS;
+extern NetmaskGroup g_proxyProtocolACL;
+extern size_t g_proxyProtocolMaximumSize;
 #ifdef HAVE_LUA_RECORDS
 extern bool g_doLuaRecord;
 extern bool g_LuaRecordSharedState;
index 705956c2f52b164fe29020b5b9b9be93329414d7..fb260e93953c45bd2ec4761f452aeef4954614de 100644 (file)
@@ -67,11 +67,52 @@ const string& DNSPacket::getString()
   return d_rawpacket;
 }
 
+string DNSPacket::getRemoteString() const
+{
+  string ret;
+
+  ret = getRemote().toString();
+
+  if (d_inner_remote) {
+    ret += "(" + d_inner_remote->toString() + ")";
+  }
+
+  if(hasEDNSSubnet()) {
+    ret += "<-" + getRealRemote().toString();
+  }
+
+  return ret;
+}
+
+string DNSPacket::getRemoteStringWithPort() const
+{
+  string ret;
+
+  ret = getRemote().toStringWithPort();
+
+  if (d_inner_remote) {
+    ret += "(" + d_inner_remote->toStringWithPort() + ")";
+  }
+
+  if(hasEDNSSubnet()) {
+    ret += "<-" + getRealRemote().toString();
+  }
+
+  return ret;
+}
+
 ComboAddress DNSPacket::getRemote() const
 {
   return d_remote;
 }
 
+ComboAddress DNSPacket::getInnerRemote() const
+{
+  if (d_inner_remote)
+    return *d_inner_remote;
+  return d_remote;
+}
+
 uint16_t DNSPacket::getRemotePort() const
 {
   return d_remote.sin4.sin_port;
@@ -369,6 +410,7 @@ std::unique_ptr<DNSPacket> DNSPacket::replyPacket() const
   r->setSocket(d_socket);
   r->d_anyLocal=d_anyLocal;
   r->setRemote(&d_remote);
+  r->d_inner_remote=d_inner_remote;
   r->setAnswer(true);  // this implies the allocation of the header
   r->setA(true); // and we are authoritative
   r->setRA(false); // no recursion available
@@ -423,7 +465,7 @@ int DNSPacket::noparse(const char *mesg, size_t length)
   d_rawpacket.assign(mesg,length); 
   if(length < 12) { 
     g_log << Logger::Debug << "Ignoring packet: too short ("<<length<<" < 12) from "
-      << d_remote.toStringWithPort()<< endl;
+      << getRemoteStringWithPort();
     return -1;
   }
   d_wantsnsid=false;
@@ -593,9 +635,15 @@ void DNSPacket::setMaxReplyLen(int bytes)
 }
 
 //! Use this to set where this packet was received from or should be sent to
-void DNSPacket::setRemote(const ComboAddress *s)
+void DNSPacket::setRemote(const ComboAddress *outer, std::optional<ComboAddress> inner)
 {
-  d_remote=*s;
+  d_remote=*outer;
+  if (inner) {
+    d_inner_remote=*inner;
+  }
+  else {
+    d_inner_remote.reset();
+  }
 }
 
 bool DNSPacket::hasEDNSSubnet() const
@@ -612,7 +660,7 @@ Netmask DNSPacket::getRealRemote() const
 {
   if(d_haveednssubnet)
     return d_eso.source;
-  return Netmask(d_remote);
+  return Netmask(getInnerRemote());
 }
 
 void DNSPacket::setSocket(Utility::sock_t sock)
index 4456847d6df55879c0ed76e5bebe5a2113e60269..0300b8e53c2a7170142539d5b4fa10f7c3b0a99b 100644 (file)
@@ -61,8 +61,9 @@ public:
   const string& getString(); //!< for serialization - just passes the whole packet
 
   // address & socket manipulation
-  void setRemote(const ComboAddress*);
+  void setRemote(const ComboAddress*, std::optional<ComboAddress> = std::nullopt);
   ComboAddress getRemote() const;
+  ComboAddress getInnerRemote() const; // for proxy protocol
   Netmask getRealRemote() const;
   ComboAddress getLocal() const
   {
@@ -73,6 +74,9 @@ public:
   }
   uint16_t getRemotePort() const;
 
+  string getRemoteString() const;
+  string getRemoteStringWithPort() const;
+
   boost::optional<ComboAddress> d_anyLocal;
 
   Utility::sock_t getSocket() const
@@ -81,7 +85,6 @@ public:
   }
   void setSocket(Utility::sock_t sock);
 
-
   // these manipulate 'd'
   void setA(bool); //!< make this packet authoritative - manipulates 'd'
   void setID(uint16_t); //!< set the DNS id of this packet - manipulates 'd'
@@ -144,6 +147,7 @@ public:
   TSIGRecordContent d_trc; //72
 
   ComboAddress d_remote; //28
+  std::optional<ComboAddress> d_inner_remote; // for proxy protocol
   TSIGHashEnum d_tsig_algo{TSIG_MD5}; //4
 
   int d_ednsRawPacketSizeLimit{-1}; // only used for Lua record
index 49dd69fd4ee33448c61fd7bca7879dba4e324636..60584d2183d036b809b52550e0d3f0d656a8461f 100644 (file)
@@ -978,7 +978,7 @@ std::vector<shared_ptr<DNSRecordContent>> luaSynth(const std::string& code, cons
   lua.writeVariable("qname", query);
   lua.writeVariable("zone", zone);
   lua.writeVariable("zoneid", zoneid);
-  lua.writeVariable("who", dnsp.getRemote());
+  lua.writeVariable("who", dnsp.getInnerRemote());
   lua.writeVariable("dh", (dnsheader*)&dnsp.d);
   lua.writeVariable("dnssecOK", dnsp.d_dnssecOk);
   lua.writeVariable("tcp", dnsp.d_tcp);
@@ -989,7 +989,7 @@ std::vector<shared_ptr<DNSRecordContent>> luaSynth(const std::string& code, cons
   }
   else {
     lua.writeVariable("ecswho", nullptr);
-    s_lua_record_ctx->bestwho = dnsp.getRemote();
+    s_lua_record_ctx->bestwho = dnsp.getInnerRemote();
   }
   lua.writeVariable("bestwho", s_lua_record_ctx->bestwho);
 
index c52341bce41ee8318cd912d5874d39426e5614b6..8f2bcd577851e059868da9ea0c1a21749683ca10 100644 (file)
@@ -32,6 +32,7 @@
 #include <sys/types.h>
 #include "responsestats.hh"
 
+#include "common_startup.hh"
 #include "dns.hh"
 #include "dnsbackend.hh"
 #include "dnspacket.hh"
@@ -40,6 +41,7 @@
 #include "logger.hh"
 #include "arguments.hh"
 #include "statbag.hh"
+#include "proxy-protocol.hh"
 
 #include "namespaces.hh"
 
@@ -301,7 +303,27 @@ bool UDPNameserver::receive(DNSPacket& packet, std::string& buffer)
     packet.d_dt.setTimeval(recvtv);
   }
   else
-    packet.d_dt.set(); // timing    
+    packet.d_dt.set(); // timing
+
+  if (g_proxyProtocolACL.match(remote)) {
+    ComboAddress psource, pdestination;
+    bool proxyProto, tcp;
+    std::vector<ProxyProtocolValue> ppvalues;
+
+    buffer.resize(len);
+    ssize_t used = parseProxyHeader(buffer, proxyProto, psource, pdestination, tcp, ppvalues);
+    if (used <= 0 || (size_t) used > g_proxyProtocolMaximumSize || (len - used) > DNSPacket::s_udpTruncationThreshold) {
+      S.inc("corrupt-packets");
+      S.ringAccount("remotes-corrupt", packet.d_remote);
+      return false;
+    }
+    buffer.erase(0, used);
+    packet.d_inner_remote = psource;
+    packet.d_tcp = tcp;
+  }
+  else {
+    packet.d_inner_remote.reset();
+  }
 
   if(packet.parse(&buffer.at(0), (size_t) len)<0) {
     S.inc("corrupt-packets");
index 45164c35d813ec177d689b870dcdc0a3ffd0e9e5..fee18b5a2c8531ae0bc483aaaee5d3e431e73b7a 100644 (file)
@@ -942,9 +942,14 @@ int PacketHandler::trySuperMaster(const DNSPacket& p, const DNSName& tsigkeyname
 int PacketHandler::trySuperMasterSynchronous(const DNSPacket& p, const DNSName& tsigkeyname)
 {
   ComboAddress remote = p.getRemote();
+  // this uses the outer (non-PROXY) remote on purpose
   if(p.hasEDNSSubnet() && pdns::isAddressTrustedNotificationProxy(remote)) {
     remote = p.getRealRemote().getNetwork();
   }
+  else {
+    // but we fall back to the inner (PROXY) remote if there is no ECS forwarded by a trusted proxy
+    remote = p.getInnerRemote();
+  }
   remote.setPort(53);
 
   Resolver::res_t nsset;
@@ -1019,34 +1024,34 @@ int PacketHandler::processNotify(const DNSPacket& p)
      if master is higher -> do stuff
   */
 
-  g_log<<Logger::Debug<<"Received NOTIFY for "<<p.qdomain<<" from "<<p.getRemote()<<endl;
+  g_log<<Logger::Debug<<"Received NOTIFY for "<<p.qdomain<<" from "<<p.getRemoteString()<<endl;
 
   if(!::arg().mustDo("secondary") && s_forwardNotify.empty()) {
-    g_log<<Logger::Warning<<"Received NOTIFY for "<<p.qdomain<<" from "<<p.getRemote()<<" but slave support is disabled in the configuration"<<endl;
+    g_log<<Logger::Warning<<"Received NOTIFY for "<<p.qdomain<<" from "<<p.getRemoteString()<<" but slave support is disabled in the configuration"<<endl;
     return RCode::Refused;
   }
 
   // Sender verification
   //
-  if(!s_allowNotifyFrom.match((ComboAddress *) &p.d_remote ) || p.d_havetsig) {
+  if(!s_allowNotifyFrom.match(p.getInnerRemote()) || p.d_havetsig) {
     if (p.d_havetsig && p.getTSIGKeyname().empty() == false) {
-        g_log<<Logger::Notice<<"Received secure NOTIFY for "<<p.qdomain<<" from "<<p.getRemote()<<", with TSIG key '"<<p.getTSIGKeyname()<<"'"<<endl;
+        g_log<<Logger::Notice<<"Received secure NOTIFY for "<<p.qdomain<<" from "<<p.getRemoteString()<<", with TSIG key '"<<p.getTSIGKeyname()<<"'"<<endl;
     } else {
-      g_log<<Logger::Warning<<"Received NOTIFY for "<<p.qdomain<<" from "<<p.getRemote()<<" but the remote is not providing a TSIG key or in allow-notify-from (Refused)"<<endl;
+      g_log<<Logger::Warning<<"Received NOTIFY for "<<p.qdomain<<" from "<<p.getRemoteString()<<" but the remote is not providing a TSIG key or in allow-notify-from (Refused)"<<endl;
       return RCode::Refused;
     }
   }
 
   if ((!::arg().mustDo("allow-unsigned-notify") && !p.d_havetsig) || p.d_havetsig) {
     if (!p.d_havetsig) {
-      g_log<<Logger::Warning<<"Received unsigned NOTIFY for "<<p.qdomain<<" from "<<p.getRemote()<<" while a TSIG key was required (Refused)"<<endl;
+      g_log<<Logger::Warning<<"Received unsigned NOTIFY for "<<p.qdomain<<" from "<<p.getRemoteString()<<" while a TSIG key was required (Refused)"<<endl;
       return RCode::Refused;
     }
     vector<string> meta;
     if (B.getDomainMetadata(p.qdomain,"AXFR-MASTER-TSIG",meta) && meta.size() > 0) {
       DNSName expected{meta[0]};
       if (p.getTSIGKeyname() != expected) {
-        g_log<<Logger::Warning<<"Received secure NOTIFY for "<<p.qdomain<<" from "<<p.getRemote()<<": expected TSIG key '"<<expected<<"', got '"<<p.getTSIGKeyname()<<"' (Refused)"<<endl;
+        g_log<<Logger::Warning<<"Received secure NOTIFY for "<<p.qdomain<<" from "<<p.getRemoteString()<<": expected TSIG key '"<<expected<<"', got '"<<p.getTSIGKeyname()<<"' (Refused)"<<endl;
         return RCode::Refused;
       }
     }
@@ -1057,13 +1062,14 @@ int PacketHandler::processNotify(const DNSPacket& p)
   DomainInfo di;
   if(!B.getDomainInfo(p.qdomain, di, false) || !di.backend) {
     if(::arg().mustDo("autosecondary")) {
-      g_log<<Logger::Warning<<"Received NOTIFY for "<<p.qdomain<<" from "<<p.getRemote()<<" for which we are not authoritative, trying supermaster"<<endl;
+      g_log<<Logger::Warning<<"Received NOTIFY for "<<p.qdomain<<" from "<<p.getRemoteString()<<" for which we are not authoritative, trying supermaster"<<endl;
       return trySuperMaster(p, p.getTSIGKeyname());
     }
-    g_log<<Logger::Notice<<"Received NOTIFY for "<<p.qdomain<<" from "<<p.getRemote()<<" for which we are not authoritative (Refused)"<<endl;
+    g_log<<Logger::Notice<<"Received NOTIFY for "<<p.qdomain<<" from "<<p.getRemoteString()<<" for which we are not authoritative (Refused)"<<endl;
     return RCode::Refused;
   }
 
+  // this uses the outer (non-PROXY) remote on purpose
   if(pdns::isAddressTrustedNotificationProxy(p.getRemote())) {
     if(di.masters.empty()) {
       g_log<<Logger::Warning<<"Received NOTIFY for "<<p.qdomain<<" from trusted-notification-proxy "<<p.getRemote()<<", zone does not have any masters defined (Refused)"<<endl;
@@ -1072,26 +1078,26 @@ int PacketHandler::processNotify(const DNSPacket& p)
     g_log<<Logger::Notice<<"Received NOTIFY for "<<p.qdomain<<" from trusted-notification-proxy "<<p.getRemote()<<endl;
   }
   else if(::arg().mustDo("primary") && di.kind == DomainInfo::Master) {
-    g_log<<Logger::Warning<<"Received NOTIFY for "<<p.qdomain<<" from "<<p.getRemote()<<" but we are master (Refused)"<<endl;
+    g_log<<Logger::Warning<<"Received NOTIFY for "<<p.qdomain<<" from "<<p.getRemoteString()<<" but we are master (Refused)"<<endl;
     return RCode::Refused;
   }
-  else if(!di.isMaster(p.getRemote())) {
-    g_log<<Logger::Warning<<"Received NOTIFY for "<<p.qdomain<<" from "<<p.getRemote()<<" which is not a master (Refused)"<<endl;
+  else if(!di.isMaster(p.getInnerRemote())) {
+    g_log<<Logger::Warning<<"Received NOTIFY for "<<p.qdomain<<" from "<<p.getRemoteString()<<" which is not a master (Refused)"<<endl;
     return RCode::Refused;
   }
 
   if(!s_forwardNotify.empty()) {
     set<string> forwardNotify(s_forwardNotify);
     for(const auto & j : forwardNotify) {
-      g_log<<Logger::Notice<<"Relaying notification of domain "<<p.qdomain<<" from "<<p.getRemote()<<" to "<<j<<endl;
+      g_log<<Logger::Notice<<"Relaying notification of domain "<<p.qdomain<<" from "<<p.getRemoteString()<<" to "<<j<<endl;
       Communicator.notify(p.qdomain,j);
     }
   }
 
   if(::arg().mustDo("secondary")) {
-    g_log<<Logger::Notice<<"Received NOTIFY for "<<p.qdomain<<" from "<<p.getRemote()<<" - queueing check"<<endl;
+    g_log<<Logger::Notice<<"Received NOTIFY for "<<p.qdomain<<" from "<<p.getRemoteString()<<" - queueing check"<<endl;
     di.receivedNotify = true;
-    Communicator.addSlaveCheckRequest(di, p.d_remote);
+    Communicator.addSlaveCheckRequest(di, p.getInnerRemote());
   }
   return 0;
 }
index b2beb16d7a905756942bb3784e42f61225084bc1..373a750e944a06e0ddc4ef6408b3e7300c775afa 100644 (file)
@@ -22,7 +22,7 @@
 
 #pragma once
 
-#include <iputils.hh>
+#include "iputils.hh"
 
 struct ProxyProtocolValue
 {
index b9c619eb2d7751012ca35132afe93c4673c95924..dfd5340cb3c9006fe8a0e8d7f6744c125ee87e0e 100644 (file)
@@ -58,6 +58,8 @@
 #include "namespaces.hh"
 #include "signingpipe.hh"
 #include "stubresolver.hh"
+#include "proxy-protocol.hh"
+#include "noinitvector.hh"
 extern AuthPacketCache PC;
 extern StatBag S;
 
@@ -240,9 +242,59 @@ void TCPNameserver::doConnection(int fd)
   try {
     int mesgsize=65535;
     boost::scoped_array<char> mesg(new char[mesgsize]);
-    
+    std::optional<ComboAddress> inner_remote;
+    bool inner_tcp = false;
+
     DLOG(g_log<<"TCP Connection accepted on fd "<<fd<<endl);
     bool logDNSQueries= ::arg().mustDo("log-dns-queries");
+    if (g_proxyProtocolACL.match(remote)) {
+      unsigned int remainingTime = 0;
+      PacketBuffer proxyData;
+      proxyData.reserve(g_proxyProtocolMaximumSize);
+      ssize_t used;
+
+      // this for-loop ends by throwing, or by having gathered a complete proxy header
+      for (;;) {
+        used = isProxyHeaderComplete(proxyData);
+        if (used < 0) {
+          ssize_t origsize = proxyData.size();
+          proxyData.resize(origsize + -used);
+          if (maxConnectionDurationReached(d_maxConnectionDuration, start, remainingTime)) {
+            throw NetworkError("Error reading PROXYv2 header from TCP client "+remote.toString()+": maximum TCP connection duration exceeded");
+          }
+
+          try {
+            readnWithTimeout(fd, &proxyData[origsize], -used, d_idleTimeout, true, remainingTime);
+          }
+          catch(NetworkError& ae) {
+            throw NetworkError("Error reading PROXYv2 header from TCP client "+remote.toString()+": "+ae.what());
+          }
+        }
+        else if (used == 0) {
+          throw NetworkError("Error reading PROXYv2 header from TCP client "+remote.toString()+": PROXYv2 header was invalid");
+        }
+        else if (static_cast<size_t>(used) > g_proxyProtocolMaximumSize) {
+          throw NetworkError("Error reading PROXYv2 header from TCP client "+remote.toString()+": PROXYv2 header too big");
+        }
+        else { // used > 0 && used <= g_proxyProtocolMaximumSize
+          break;
+        }
+      }
+      ComboAddress psource, pdestination;
+      bool proxyProto, tcp;
+      std::vector<ProxyProtocolValue> ppvalues;
+
+      used = parseProxyHeader(proxyData, proxyProto, psource, pdestination, tcp, ppvalues);
+      if (used <= 0) {
+        throw NetworkError("Error reading PROXYv2 header from TCP client "+remote.toString()+": PROXYv2 header was invalid");
+      }
+      if (static_cast<size_t>(used) > g_proxyProtocolMaximumSize) {
+        throw NetworkError("Error reading PROXYv2 header from TCP client "+remote.toString()+": PROXYv2 header was oversized");
+      }
+      inner_remote = psource;
+      inner_tcp = tcp;
+    }
+
     for(;;) {
       unsigned int remainingTime = 0;
       transactions++;
@@ -288,6 +340,10 @@ void TCPNameserver::doConnection(int fd)
       packet=make_unique<DNSPacket>(true);
       packet->setRemote(&remote);
       packet->d_tcp=true;
+      if (inner_remote) {
+        packet->d_inner_remote = inner_remote;
+        packet->d_tcp = inner_tcp;
+      }
       packet->setSocket(fd);
       if(packet->parse(mesg.get(), pktlen)<0)
         break;
@@ -305,12 +361,7 @@ void TCPNameserver::doConnection(int fd)
       std::unique_ptr<DNSPacket> reply; 
       auto cached = make_unique<DNSPacket>(false);
       if(logDNSQueries)  {
-        string remote_text;
-        if(packet->hasEDNSSubnet())
-          remote_text = packet->getRemote().toString() + "<-" + packet->getRealRemote().toString();
-        else
-          remote_text = packet->getRemote().toString();
-        g_log << Logger::Notice<<"TCP Remote "<< remote_text <<" wants '" << packet->qdomain<<"|"<<packet->qtype.toString() <<
+        g_log << Logger::Notice<<"TCP Remote "<< packet->getRemoteString() <<" wants '" << packet->qdomain<<"|"<<packet->qtype.toString() <<
         "', do = " <<packet->d_dnssecOk <<", bufsize = "<< packet->getMaxReplyLen();
       }
 
@@ -383,7 +434,7 @@ bool TCPNameserver::canDoAXFR(std::unique_ptr<DNSPacket>& q, bool isAXFR)
   if(::arg().mustDo("disable-axfr"))
     return false;
 
-  string logPrefix=string(isAXFR ? "A" : "I")+"XFR-out zone '"+q->qdomain.toLogString()+"', client '"+q->getRemote().toStringWithPort()+"', ";
+  string logPrefix=string(isAXFR ? "A" : "I")+"XFR-out zone '"+q->qdomain.toLogString()+"', client '"+q->getInnerRemote().toStringWithPort()+"', ";
 
   if(q->d_havetsig) { // if you have one, it must be good
     TSIGRecordContent trc;
@@ -407,7 +458,7 @@ bool TCPNameserver::canDoAXFR(std::unique_ptr<DNSPacket>& q, bool isAXFR)
   }
   
   // cerr<<"checking allow-axfr-ips"<<endl;
-  if(!(::arg()["allow-axfr-ips"].empty()) && d_ng.match( (ComboAddress *) &q->d_remote )) {
+  if(!(::arg()["allow-axfr-ips"].empty()) && d_ng.match( q->getInnerRemote() )) {
     g_log<<Logger::Notice<<logPrefix<<"allowed: client IP is in allow-axfr-ips"<<endl;
     return true;
   }
@@ -436,7 +487,7 @@ bool TCPNameserver::canDoAXFR(std::unique_ptr<DNSPacket>& q, bool isAXFR)
           vector<string> nsips=fns.lookup(j, s_P->getBackend());
           for(const auto & nsip : nsips) {
             // cerr<<"got "<<*k<<" from AUTO-NS"<<endl;
-            if(nsip == q->getRemote().toString())
+            if(nsip == q->getInnerRemote().toString())
             {
               // cerr<<"got AUTO-NS hit"<<endl;
               g_log<<Logger::Notice<<logPrefix<<"allowed: client IP is in NSset"<<endl;
@@ -448,7 +499,7 @@ bool TCPNameserver::canDoAXFR(std::unique_ptr<DNSPacket>& q, bool isAXFR)
       else
       {
         Netmask nm = Netmask(i);
-        if(nm.match( (ComboAddress *) &q->d_remote ))
+        if(nm.match( q->getInnerRemote() ))
         {
           g_log<<Logger::Notice<<logPrefix<<"allowed: client IP is in per-zone ACL"<<endl;
           // cerr<<"hit!"<<endl;
@@ -460,7 +511,7 @@ bool TCPNameserver::canDoAXFR(std::unique_ptr<DNSPacket>& q, bool isAXFR)
 
   extern CommunicatorClass Communicator;
 
-  if(Communicator.justNotified(q->qdomain, q->getRemote().toString())) { // we just notified this ip
+  if(Communicator.justNotified(q->qdomain, q->getInnerRemote().toString())) { // we just notified this ip
     g_log<<Logger::Notice<<logPrefix<<"allowed: client IP is from recently notified secondary"<<endl;
     return true;
   }
@@ -491,7 +542,7 @@ namespace {
 /** do the actual zone transfer. Return 0 in case of error, 1 in case of success */
 int TCPNameserver::doAXFR(const DNSName &target, std::unique_ptr<DNSPacket>& q, int outsock)
 {
-  string logPrefix="AXFR-out zone '"+target.toLogString()+"', client '"+q->getRemote().toStringWithPort()+"', ";
+  string logPrefix="AXFR-out zone '"+target.toLogString()+"', client '"+q->getRemoteString()+"', ";
 
   std::unique_ptr<DNSPacket> outpacket= getFreshAXFRPacket(q);
   if(q->d_dnssecOk)
@@ -1023,7 +1074,7 @@ int TCPNameserver::doAXFR(const DNSName &target, std::unique_ptr<DNSPacket>& q,
 
 int TCPNameserver::doIXFR(std::unique_ptr<DNSPacket>& q, int outsock)
 {
-  string logPrefix="IXFR-out zone '"+q->qdomain.toLogString()+"', client '"+q->getRemote().toStringWithPort()+"', ";
+  string logPrefix="IXFR-out zone '"+q->qdomain.toLogString()+"', client '"+q->getRemoteString()+"', ";
 
   std::unique_ptr<DNSPacket> outpacket=getFreshAXFRPacket(q);
   if(q->d_dnssecOk)
index 366921a91e560c9df4a318b497134800756c5285..368682fa3d2f2e92ccd2aed0e61cb468468fd215 100644 (file)
@@ -49,7 +49,6 @@ public:
 private:
 
   static void sendPacket(std::unique_ptr<DNSPacket>& p, int outsock, bool last=true);
-  static int readLength(int fd, ComboAddress *remote);
   static void getQuestion(int fd, char *mesg, int pktlen, const ComboAddress& remote, unsigned int totalTime);
   static int doAXFR(const DNSName &target, std::unique_ptr<DNSPacket>& q, int outsock);
   static int doIXFR(std::unique_ptr<DNSPacket>& q, int outsock);
index 2f6044dbbb221b198195a822ef0b2ac3a568ec8d..0517b076f67b3a485310e972eeb635641e8b421e 100644 (file)
@@ -11,6 +11,8 @@
 #include <utility>
 
 extern vector<ComboAddress> g_localaddresses;
+NetmaskGroup g_proxyProtocolACL;
+size_t g_proxyProtocolMaximumSize = 512;
 
 BOOST_AUTO_TEST_SUITE(test_nameserver_cc)
 
diff --git a/regression-tests.auth-py/proxyprotocol.py b/regression-tests.auth-py/proxyprotocol.py
new file mode 120000 (symlink)
index 0000000..2a3d79b
--- /dev/null
@@ -0,0 +1 @@
+../regression-tests.common/proxyprotocol.py
\ No newline at end of file
diff --git a/regression-tests.auth-py/test_ProxyProtocol.py b/regression-tests.auth-py/test_ProxyProtocol.py
new file mode 100644 (file)
index 0000000..c3861ce
--- /dev/null
@@ -0,0 +1,227 @@
+import dns
+import os
+import socket
+import struct
+import threading
+import time
+import unittest
+
+from authtests import AuthTest
+from proxyprotocol import ProxyProtocol
+
+class TestProxyProtocolLuaRecords(AuthTest):
+    _config_template = """
+launch=bind
+any-to-tcp=no
+proxy-protocol-from=127.0.0.1
+"""
+
+    _zones = {
+        'example.org': """
+example.org.                 3600 IN SOA  {soa}
+example.org.                 3600 IN NS   ns1.example.org.
+example.org.                 3600 IN NS   ns2.example.org.
+ns1.example.org.             3600 IN A    {prefix}.10
+ns2.example.org.             3600 IN A    {prefix}.11
+
+myip.example.org.            3600 IN LUA  A     "who:toString()"
+        """
+    }
+
+    @classmethod
+    def setUpClass(cls):
+        super(TestProxyProtocolLuaRecords, cls).setUpClass()
+
+    def testWhoAmI(self):
+        """
+        See if LUA who picks up the inner address from the PROXY protocol
+        """
+        
+        # first test with an unproxied query - should get ignored
+        query = dns.message.make_query('myip.example.org', 'A')
+
+        res = self.sendUDPQuery(query)
+
+        self.assertEqual(res, None)     # query was ignored correctly
+
+
+        # now send a proxied query
+        queryPayload = query.to_wire()
+        ppPayload = ProxyProtocol.getPayload(False, False, False, "192.0.2.1", "10.1.2.3", 12345, 53, [])
+        payload = ppPayload + queryPayload
+
+        # UDP
+        self._sock.settimeout(2.0)
+
+        try:
+            self._sock.send(payload)
+            data = self._sock.recv(4096)
+        except socket.timeout:
+            data = None
+        finally:
+            self._sock.settimeout(None)
+
+        res = None
+        if data:
+            res = dns.message.from_wire(data)
+
+        expected = [dns.rrset.from_text('myip.example.org.', 0, dns.rdataclass.IN, 'A', '192.0.2.1')]
+        self.assertRcodeEqual(res, dns.rcode.NOERROR)
+        self.assertEqual(res.answer, expected)
+
+        # TCP
+        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        sock.settimeout(2.0)
+        sock.connect(("127.0.0.1", self._authPort))
+
+        try:
+            sock.send(ppPayload)
+            sock.send(struct.pack("!H", len(queryPayload)))
+            sock.send(queryPayload)
+            data = sock.recv(2)
+            if data:
+                (datalen,) = struct.unpack("!H", data)
+                data = sock.recv(datalen)
+        except socket.timeout as e:
+            print("Timeout: %s" % (str(e)))
+            data = None
+        except socket.error as e:
+            print("Network error: %s" % (str(e)))
+            data = None
+        finally:
+            sock.close()
+
+        res = None
+        if data:
+            res = dns.message.from_wire(data)
+
+        self.assertRcodeEqual(res, dns.rcode.NOERROR)
+        self.assertEqual(res.answer, expected)
+
+class TestProxyProtocolNOTIFY(AuthTest):
+    _config_template = """
+launch=bind
+any-to-tcp=no
+proxy-protocol-from=127.0.0.1
+secondary
+"""
+
+    _zones = { 'example.org': '192.0.2.1',
+               'example.com': '192.0.2.2'
+    }
+
+    @classmethod
+    def generateAuthZone(cls, confdir, zonename, zonecontent):
+        try:
+            os.unlink(os.path.join(confdir, '%s.zone' % zonename))
+        except:
+            pass
+
+    @classmethod
+    def generateAuthNamedConf(cls, confdir, zones):
+        with open(os.path.join(confdir, 'named.conf'), 'w') as namedconf:
+            namedconf.write("""
+options {
+    directory "%s";
+};""" % confdir)
+            for zonename in zones:
+                zone = '.' if zonename == 'ROOT' else zonename
+
+                namedconf.write("""
+        zone "%s" {
+            type slave;
+            file "%s.zone";
+            masters { %s; };
+        };""" % (zone, zonename, cls._zones[zone]))
+
+
+    @classmethod
+    def setUpClass(cls):
+        super(TestProxyProtocolNOTIFY, cls).setUpClass()
+
+    def testNOTIFY(self):
+        """
+        Check that NOTIFY is properly accepted/rejected based on the PROXY header inner address
+        """
+
+        query = dns.message.make_query('example.org', 'SOA')
+        query.set_opcode(dns.opcode.NOTIFY)
+
+        queryPayload = query.to_wire()
+
+        for task in ('192.0.2.1', dns.rcode.NOERROR), ('192.0.2.2', dns.rcode.REFUSED):
+            ip, expectedrcode = task
+
+            ppPayload = ProxyProtocol.getPayload(False, False, False, ip, "10.1.2.3", 12345, 53, [])
+            payload = ppPayload + queryPayload
+
+            self._sock.settimeout(2.0)
+
+            try:
+                self._sock.send(payload)
+                data = self._sock.recv(4096)
+            except socket.timeout:
+                data = None
+            finally:
+                self._sock.settimeout(None)
+
+            res = None
+            if data:
+                res = dns.message.from_wire(data)
+
+            self.assertRcodeEqual(res, expectedrcode)
+
+
+class TestProxyProtocolAXFRACL(AuthTest):
+    _config_template = """
+launch=bind
+any-to-tcp=no
+proxy-protocol-from=127.0.0.1
+allow-axfr-ips=192.0.2.53
+"""
+
+    @classmethod
+    def setUpClass(cls):
+        super(TestProxyProtocolAXFRACL, cls).setUpClass()
+
+    def testAXFR(self):
+        """
+        Check that AXFR is properly accepted/rejected based on the PROXY header inner address
+        """
+
+        query = dns.message.make_query('example.org', 'AXFR')
+
+        queryPayload = query.to_wire()
+
+        for task in ('192.0.2.1', dns.rcode.NOTAUTH), ('127.0.0.1', dns.rcode.NOTAUTH), ('192.0.2.53', dns.rcode.NOERROR):
+            ip, expectedrcode = task
+
+            ppPayload = ProxyProtocol.getPayload(False, True, False, ip, "10.1.2.3", 12345, 53, [])
+
+            # TCP
+            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+            sock.settimeout(2.0)
+            sock.connect(("127.0.0.1", self._authPort))
+
+            try:
+                sock.send(ppPayload)
+                sock.send(struct.pack("!H", len(queryPayload)))
+                sock.send(queryPayload)
+                data = sock.recv(2)
+                if data:
+                    (datalen,) = struct.unpack("!H", data)
+                    data = sock.recv(datalen)
+            except socket.timeout as e:
+                print("Timeout: %s" % (str(e)))
+                data = None
+            except socket.error as e:
+                print("Network error: %s" % (str(e)))
+                data = None
+            finally:
+                sock.close()
+
+            res = None
+            if data:
+                res = dns.message.from_wire(data)
+
+            self.assertRcodeEqual(res, expectedrcode)