]> granicus.if.org Git - pdns/commitdiff
Add initial XPF support to the recursor and dnsdist
authorRemi Gacogne <remi.gacogne@powerdns.com>
Fri, 11 Aug 2017 12:41:31 +0000 (14:41 +0200)
committerPieter Lexis <pieter.lexis@powerdns.com>
Mon, 22 Jan 2018 22:02:36 +0000 (23:02 +0100)
19 files changed:
pdns/dnsdist-lua.cc
pdns/dnsdist-tcp.cc
pdns/dnsdist.cc
pdns/dnsdist.hh
pdns/dnsdistdist/Makefile.am
pdns/dnsdistdist/docs/reference/config.rst
pdns/dnsdistdist/xpf.cc [new symlink]
pdns/dnsdistdist/xpf.hh [new symlink]
pdns/iputils.hh
pdns/pdns_recursor.cc
pdns/recursordist/Makefile.am
pdns/recursordist/docs/settings.rst
pdns/recursordist/test-ednsoptions_cc.cc
pdns/recursordist/test-xpf_cc.cc [new file with mode: 0644]
pdns/recursordist/xpf.cc [new symlink]
pdns/recursordist/xpf.hh [new symlink]
pdns/xpf.cc [new file with mode: 0644]
pdns/xpf.hh [new file with mode: 0644]
regression-tests.dnsdist/test_XPF.py [new file with mode: 0644]

index 03e988f5cdca272bad35c8413da64156fe992556..866ae4050ee1d6f7691b8c762d749f35fa90e5e6 100644 (file)
@@ -307,6 +307,10 @@ void setupLuaConfig(bool client)
 
                        if(vars.count("ipBindAddrNoPort")) {
                          ret->ipBindAddrNoPort=boost::get<bool>(vars["ipBindAddrNoPort"]);
+                        }
+
+                       if(vars.count("addXPF")) {
+                         ret->addXPF=boost::get<bool>(vars["addXPF"]);
                        }
 
                        if(vars.count("maxCheckFailures")) {
index f23b4c15845a4b177e62a09ee53b6eb15bd5785d..54586a4375747292dd761ea349c920a932ebb484 100644 (file)
@@ -440,6 +440,10 @@ void* tcpClientThread(int pipefd)
           break;
         }
 
+        if (dq.addXPF && ds->addXPF) {
+          addXPF(dq);
+        }
+
        int dsock = -1;
        uint16_t downstreamFailures=0;
 #ifdef MSG_FASTOPEN
index 1ff98fd3677c4767f798a09bdbe8d3ac47c6350d..293f4616ce83b2d1b6c81e36d1572dbbb4e7a98a 100644 (file)
  * along with this program; if not, write to the Free Software
  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  */
-#include "dnsdist.hh"
-#include "dnsdist-ecs.hh"
-#include "sstuff.hh"
-#include "misc.hh"
-#include <netinet/tcp.h>
+
+#include "config.h"
+
+#include <fstream>
+#include <getopt.h>
+#include <grp.h>
 #include <limits>
-#include "dolog.hh"
+#include <netinet/tcp.h>
+#include <pwd.h>
+#include <sys/resource.h>
+#include <unistd.h>
 
 #if defined (__OpenBSD__) || defined(__NetBSD__)
 #include <readline/readline.h>
 #include <editline/readline.h>
 #endif
 
-#include "dnsname.hh"
-#include "dnswriter.hh"
-#include "base64.hh"
-#include <fstream>
-#include "delaypipe.hh"
-#include <unistd.h>
-#include "sodcrypto.hh"
-#include "dnsdist-lua.hh"
-#include <grp.h>
-#include <pwd.h>
-#include "lock.hh"
-#include <getopt.h>
-#include <sys/resource.h>
-#include "dnsdist-cache.hh"
-#include "gettime.hh"
-#include "ednsoptions.hh"
-
 #ifdef HAVE_SYSTEMD
 #include <systemd/sd-daemon.h>
 #endif
 
+#include "dnsdist.hh"
+#include "dnsdist-cache.hh"
+#include "dnsdist-ecs.hh"
+#include "dnsdist-lua.hh"
+
+#include "base64.hh"
+#include "delaypipe.hh"
+#include "dolog.hh"
+#include "dnsname.hh"
+#include "dnswriter.hh"
+#include "ednsoptions.hh"
+#include "gettime.hh"
+#include "lock.hh"
+#include "misc.hh"
+#include "sodcrypto.hh"
+#include "sstuff.hh"
+#include "xpf.hh"
+
+#ifdef HAVE_PROTOBUF
 thread_local boost::uuids::random_generator t_uuidGenerator;
+#endif
 
 /* Known sins:
 
@@ -1094,6 +1101,38 @@ static ssize_t udpClientSendRequestToBackend(DownstreamState* ss, const int sd,
   return result;
 }
 
+bool addXPF(DNSQuestion& dq)
+{
+  std::string payload = generateXPFPayload(dq.tcp, *dq.remote, *dq.local);
+  uint8_t root = '\0';
+  dnsrecordheader drh;
+  drh.d_type = htons(QType::XPF);
+  drh.d_class = htons(QClass::IN);
+  drh.d_ttl = 0;
+  drh.d_clen = htons(payload.size());
+  size_t recordHeaderLen = sizeof(root) + sizeof(drh);
+
+  size_t available = dq.size - dq.len;
+
+  if ((payload.size() + recordHeaderLen) > available) {
+    return false;
+  }
+
+  size_t pos = dq.len;
+  memcpy(reinterpret_cast<char*>(dq.dh) + pos, &root, sizeof(root));
+  pos += sizeof(root);
+  memcpy(reinterpret_cast<char*>(dq.dh) + pos, &drh, sizeof(drh));
+  pos += sizeof(drh);
+  memcpy(reinterpret_cast<char*>(dq.dh) + pos, payload.data(), payload.size());
+  pos += payload.size();
+
+  dq.len = pos;
+
+  dq.dh->arcount = htons(ntohs(dq.dh->arcount) + 1);
+
+  return true;
+}
+
 static bool isUDPQueryAcceptable(ClientState& cs, LocalHolders& holders, const struct msghdr* msgh, const ComboAddress& remote, ComboAddress& dest)
 {
   if (msgh->msg_flags & MSG_TRUNC) {
@@ -1350,6 +1389,10 @@ static void processUDPQuery(ClientState& cs, LocalHolders& holders, const struct
       return;
     }
 
+    if (dq.addXPF && ss->addXPF) {
+      addXPF(dq);
+    }
+
     ss->queries++;
 
     unsigned int idOffset = (ss->idOffset++) % ss->idStates.size();
@@ -1499,6 +1542,7 @@ static void MultipleMessagesUDPClientThread(ClientState* cs, LocalHolders& holde
 }
 #endif /* defined(HAVE_RECVMMSG) && defined(HAVE_SENDMMSG) && defined(MSG_WAITFORONE) */
 
+
 // listens to incoming queries, sends out to downstream servers, noting the intended return path
 static void* udpClientThread(ClientState* cs)
 try
index aff57f207486e37e7bf6d0ca83633102970ecdb6..06973257ba5d58e9bdd05c5b7b91130bd05959c0 100644 (file)
@@ -146,6 +146,7 @@ struct DNSQuestion
   bool skipCache{false};
   bool ecsOverride;
   bool useECS{true};
+  bool addXPF{true};
 };
 
 struct DNSResponse : DNSQuestion
@@ -631,6 +632,7 @@ struct DownstreamState
   bool mustResolve{false};
   bool upStatus{false};
   bool useECS{false};
+  bool addXPF{false};
   bool setCD{false};
   std::atomic<bool> connected{false};
   bool tcpFastOpen{false};
@@ -867,6 +869,8 @@ int handleDnsCryptQuery(DnsCryptContext* ctx, char* packet, uint16_t len, std::s
 bool encryptResponse(char* response, uint16_t* responseLen, size_t responseSize, bool tcp, std::shared_ptr<DnsCryptQuery> dnsCryptQuery, dnsheader** dh, dnsheader* dhCopy);
 #endif
 
+bool addXPF(DNSQuestion& dq);
+
 #include "dnsdist-snmp.hh"
 
 extern bool g_snmpEnabled;
index 867f9b559a2dca0c30e2254bfe5fde88c62118b9..30b4eddab5feebe3aa85f6c2e4b4386f4131b9af 100644 (file)
@@ -126,6 +126,7 @@ dnsdist_SOURCES = \
        sstuff.hh \
        statnode.cc statnode.hh \
        tcpiohandler.cc tcpiohandler.hh \
+       xpf.cc xpf.hh \
        ext/luawrapper/include/LuaContext.hpp \
        ext/json11/json11.cpp \
        ext/json11/json11.hpp \
@@ -224,7 +225,8 @@ testrunner_SOURCES = \
        sholder.hh \
        sodcrypto.cc \
        sstuff.hh \
-       testrunner.cc
+       testrunner.cc \
+       xpf.cc xpf.hh
 
 testrunner_LDFLAGS = \
        $(AM_LDFLAGS) \
index 93ac4d21e2aa5e66d67b0a9cd7c5b13f91276f34..b83f05b3618f71a3531151baa40931beba3156fa 100644 (file)
@@ -245,6 +245,8 @@ Servers
 
     newServer({
       address="IP:PORT",     -- IP and PORT of the backend server (mandatory)
+      addXPF=BOOL,           -- Add the client's IP address and port to the query, along with the original destination address and port,
+                             -- using the experimental XPF record from `draft-bellis-dnsop-xpf`. Default to false
       qps=NUM,               -- Limit the number of queries per second to NUM, when using the `firstAvailable` policy
       order=NUM,             -- The order of this server, used by the `leastOustanding` and `firstAvailable` policies
       weight=NUM,            -- The weight of this server, used by the `wrandom` and `whashed` policies
diff --git a/pdns/dnsdistdist/xpf.cc b/pdns/dnsdistdist/xpf.cc
new file mode 120000 (symlink)
index 0000000..98ab722
--- /dev/null
@@ -0,0 +1 @@
+../xpf.cc
\ No newline at end of file
diff --git a/pdns/dnsdistdist/xpf.hh b/pdns/dnsdistdist/xpf.hh
new file mode 120000 (symlink)
index 0000000..023c8c4
--- /dev/null
@@ -0,0 +1 @@
+../xpf.hh
\ No newline at end of file
index de0cedf897bf9aa3ec64a5c98fbfe2309073cb8d..b80aa800f344417d348c2ed39255d7e75c0d72ec 100644 (file)
@@ -188,6 +188,8 @@ union ComboAddress {
     sin4.sin_family=AF_INET;
     sin4.sin_addr.s_addr=0;
     sin4.sin_port=0;
+    sin6.sin6_scope_id = 0;
+    sin6.sin6_flowinfo = 0;
   }
 
   ComboAddress(const struct sockaddr *sa, socklen_t salen) {
@@ -267,7 +269,7 @@ union ComboAddress {
   string toString() const
   {
     char host[1024];
-    int retval;
+    int retval = 0;
     if(sin4.sin_family && !(retval = getnameinfo((struct sockaddr*) this, getSocklen(), host, sizeof(host),0, 0, NI_NUMERICHOST)))
       return host;
     else
@@ -310,28 +312,30 @@ inline ComboAddress makeComboAddress(const string& str)
   return address;
 }
 
-inline ComboAddress makeComboAddressFromRaw(uint8_t version, const string &str)
+inline ComboAddress makeComboAddressFromRaw(uint8_t version, const char* raw, size_t len)
 {
   ComboAddress address;
-  size_t len;
 
   if (version == 4) {
-    len = 4;
-    address.sin4.sin_family=AF_INET;
-    if(str.size() != len) throw NetmaskException("invalid raw address length");
-    memcpy(&address.sin4.sin_addr, str.c_str(), len);
+    address.sin4.sin_family = AF_INET;
+    if (len != sizeof(address.sin4.sin_addr)) throw NetmaskException("invalid raw address length");
+    memcpy(&address.sin4.sin_addr, raw, sizeof(address.sin4.sin_addr));
   }
   else if (version == 6) {
-    len = 16;
-    address.sin4.sin_family=AF_INET6;
-    if(str.size() != len) throw NetmaskException("invalid raw address length");
-    memcpy(&address.sin6.sin6_addr, str.c_str(), len);
+    address.sin6.sin6_family = AF_INET6;
+    if (len != sizeof(address.sin6.sin6_addr)) throw NetmaskException("invalid raw address length");
+    memcpy(&address.sin6.sin6_addr, raw, sizeof(address.sin6.sin6_addr));
   }
   else throw NetmaskException("invalid address family");
 
   return address;
 }
 
+inline ComboAddress makeComboAddressFromRaw(uint8_t version, const string &str)
+{
+  return makeComboAddressFromRaw(version, str.c_str(), str.size());
+}
+
 /** This class represents a netmask and can be queried to see if a certain
     IP address is matched by this mask */
 class Netmask
index d96a64be9734dc6793b4bcf3966a16d5a4d411c3..ff3ec5ca55444ab30cef12f3504dbe83067232bb 100644 (file)
@@ -94,6 +94,8 @@
 
 #include "namespaces.hh"
 
+#include "xpf.hh"
+
 typedef map<ComboAddress, uint32_t, ComboAddress::addressOnlyLessThan> tcpClientCounts_t;
 
 static thread_local std::shared_ptr<RecursorLua4> t_pdl;
@@ -136,6 +138,7 @@ static vector<ComboAddress> g_localQueryAddresses4, g_localQueryAddresses6;
 static AtomicCounter 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 NetmaskGroup g_XPFAcl;
 static size_t g_tcpMaxQueriesPerConn;
 static uint64_t g_latencyStatSize;
 static uint32_t g_disthashseed;
@@ -181,10 +184,15 @@ struct DNSComboWriter {
   DNSComboWriter(const char* data, uint16_t len, const struct timeval& now) : d_mdp(true, data, len), d_now(now),
                                                                                                         d_tcp(false), d_socket(-1)
   {}
-  MOADNSParser d_mdp;
-  void setRemote(const ComboAddress* sa)
+
+  void setRemote(const ComboAddress& sa)
+  {
+    d_remote=sa;
+  }
+
+  void setSource(const ComboAddress& sa)
   {
-    d_remote=*sa;
+    d_source=sa;
   }
 
   void setLocal(const ComboAddress& sa)
@@ -192,6 +200,10 @@ struct DNSComboWriter {
     d_local=sa;
   }
 
+  void setDestination(const ComboAddress& sa)
+  {
+    d_destination=sa;
+  }
 
   void setSocket(int sock)
   {
@@ -200,11 +212,26 @@ struct DNSComboWriter {
 
   string getRemote() const
   {
-    return d_remote.toString();
+    if (d_source == d_remote) {
+      return d_source.toStringWithPort();
+    }
+    return d_source.toStringWithPort() + " (proxied by " + d_remote.toStringWithPort() + ")";
   }
 
+  MOADNSParser d_mdp;
   struct timeval d_now;
-  ComboAddress d_remote, d_local;
+  /* Remote client, might differ from d_source
+     in case of XPF, in which case d_source holds
+     the IP of the client and d_remote of the proxy
+  */
+  ComboAddress d_remote;
+  ComboAddress d_source;
+  /* Destination address, might differ from
+     d_destination in case of XPF, in which case
+     d_destination holds the IP of the proxy and
+     d_local holds our own. */
+  ComboAddress d_local;
+  ComboAddress d_destination;
 #ifdef HAVE_PROTOBUF
   boost::uuids::uuid d_uuid;
   string d_requestorId;
@@ -642,7 +669,7 @@ static void updateResponseStats(int res, const ComboAddress& remote, unsigned in
 static string makeLoginfo(DNSComboWriter* dc)
 try
 {
-  return "("+dc->d_mdp.d_qname.toLogString()+"/"+DNSRecordContent::NumberToType(dc->d_mdp.d_qtype)+" from "+(dc->d_remote.toString())+")";
+  return "("+dc->d_mdp.d_qname.toLogString()+"/"+DNSRecordContent::NumberToType(dc->d_mdp.d_qtype)+" from "+(dc->getRemote())+")";
 }
 catch(...)
 {
@@ -764,9 +791,9 @@ static void startDoResolve(void *p)
     RecProtoBufMessage pbMessage(RecProtoBufMessage::Response);
 #ifdef HAVE_PROTOBUF
     if (luaconfsLocal->protobufServer) {
-      Netmask requestorNM(dc->d_remote, dc->d_remote.sin4.sin_family == AF_INET ? luaconfsLocal->protobufMaskV4 : luaconfsLocal->protobufMaskV6);
+      Netmask requestorNM(dc->d_source, dc->d_source.sin4.sin_family == AF_INET ? luaconfsLocal->protobufMaskV4 : luaconfsLocal->protobufMaskV6);
       const ComboAddress& requestor = requestorNM.getMaskedNetwork();
-      pbMessage.update(dc->d_uuid, &requestor, &dc->d_local, dc->d_tcp, dc->d_mdp.d_header.id);
+      pbMessage.update(dc->d_uuid, &requestor, &dc->d_destination, dc->d_tcp, dc->d_mdp.d_header.id);
       pbMessage.setEDNSSubnet(dc->d_ednssubnet.source, dc->d_ednssubnet.source.isIpv4() ? luaconfsLocal->protobufMaskV4 : luaconfsLocal->protobufMaskV6);
       pbMessage.setQuestion(dc->d_mdp.d_qname, dc->d_mdp.d_qtype, dc->d_mdp.d_qclass);
     }
@@ -818,7 +845,7 @@ static void startDoResolve(void *p)
     int res = RCode::NoError;
     DNSFilterEngine::Policy appliedPolicy;
     DNSRecord spoofed;
-    RecursorLua4::DNSQuestion dq(dc->d_remote, dc->d_local, dc->d_mdp.d_qname, dc->d_mdp.d_qtype, dc->d_tcp, variableAnswer, wantsRPZ);
+    RecursorLua4::DNSQuestion dq(dc->d_source, dc->d_destination, dc->d_mdp.d_qname, dc->d_mdp.d_qtype, dc->d_tcp, variableAnswer, wantsRPZ);
     dq.ednsFlags = &edo.d_Z;
     dq.ednsOptions = &dc->d_ednsOpts;
     dq.tag = dc->d_tag;
@@ -865,7 +892,7 @@ static void startDoResolve(void *p)
 
     // Check if the query has a policy attached to it
     if (wantsRPZ) {
-      appliedPolicy = luaconfsLocal->dfe.getQueryPolicy(dc->d_mdp.d_qname, dc->d_remote, sr.d_discardedPolicies);
+      appliedPolicy = luaconfsLocal->dfe.getQueryPolicy(dc->d_mdp.d_qname, dc->d_source, sr.d_discardedPolicies);
     }
 
     // if there is a RecursorLua active, and it 'took' the query in preResolve, we don't launch beginResolve
@@ -1054,14 +1081,14 @@ static void startDoResolve(void *p)
       if(!shouldNotValidate && sr.isDNSSECValidationRequested()) {
         try {
           if(sr.doLog()) {
-            L<<Logger::Warning<<"Starting validation of answer to "<<dc->d_mdp.d_qname<<"|"<<QType(dc->d_mdp.d_qtype).getName()<<" for "<<dc->d_remote.toStringWithPort()<<endl;
+            L<<Logger::Warning<<"Starting validation of answer to "<<dc->d_mdp.d_qname<<"|"<<QType(dc->d_mdp.d_qtype).getName()<<" for "<<dc->getRemote()<<endl;
           }
 
           auto state = sr.getValidationState();
 
           if(state == Secure) {
             if(sr.doLog()) {
-              L<<Logger::Warning<<"Answer to "<<dc->d_mdp.d_qname<<"|"<<QType(dc->d_mdp.d_qtype).getName()<<" for "<<dc->d_remote.toStringWithPort()<<" validates correctly"<<endl;
+              L<<Logger::Warning<<"Answer to "<<dc->d_mdp.d_qname<<"|"<<QType(dc->d_mdp.d_qtype).getName()<<" for "<<dc->getRemote()<<" validates correctly"<<endl;
             }
             
             // Is the query source interested in the value of the ad-bit?
@@ -1070,14 +1097,14 @@ static void startDoResolve(void *p)
           }
           else if(state == Insecure) {
             if(sr.doLog()) {
-              L<<Logger::Warning<<"Answer to "<<dc->d_mdp.d_qname<<"|"<<QType(dc->d_mdp.d_qtype).getName()<<" for "<<dc->d_remote.toStringWithPort()<<" validates as Insecure"<<endl;
+              L<<Logger::Warning<<"Answer to "<<dc->d_mdp.d_qname<<"|"<<QType(dc->d_mdp.d_qtype).getName()<<" for "<<dc->getRemote()<<" validates as Insecure"<<endl;
             }
             
             pw.getHeader()->ad=0;
           }
           else if(state == Bogus) {
             if(g_dnssecLogBogus || sr.doLog() || g_dnssecmode == DNSSECMode::ValidateForLog) {
-              L<<Logger::Warning<<"Answer to "<<dc->d_mdp.d_qname<<"|"<<QType(dc->d_mdp.d_qtype).getName()<<" for "<<dc->d_remote.toStringWithPort()<<" validates as Bogus"<<endl;
+              L<<Logger::Warning<<"Answer to "<<dc->d_mdp.d_qname<<"|"<<QType(dc->d_mdp.d_qtype).getName()<<" for "<<dc->getRemote()<<" validates as Bogus"<<endl;
             }
             
             // Does the query or validation mode sending out a SERVFAIL on validation errors?
@@ -1105,7 +1132,7 @@ static void startDoResolve(void *p)
 
       if(ret.size()) {
         orderAndShuffle(ret);
-       if(auto sl = luaconfsLocal->sortlist.getOrderCmp(dc->d_remote)) {
+       if(auto sl = luaconfsLocal->sortlist.getOrderCmp(dc->d_source)) {
          stable_sort(ret.begin(), ret.end(), *sl);
          variableAnswer=true;
        }
@@ -1157,7 +1184,7 @@ static void startDoResolve(void *p)
     }
 
     g_rs.submitResponse(dc->d_mdp.d_qtype, packet.size(), !dc->d_tcp);
-    updateResponseStats(res, dc->d_remote, packet.size(), &dc->d_mdp.d_qname, dc->d_mdp.d_qtype);
+    updateResponseStats(res, dc->d_source, packet.size(), &dc->d_mdp.d_qname, dc->d_mdp.d_qtype);
 #ifdef HAVE_PROTOBUF
     if (luaconfsLocal->protobufServer && (!luaconfsLocal->protobufTaggedOnly || (appliedPolicy.d_name && !appliedPolicy.d_name->empty()) || !dc->d_policyTags.empty())) {
       pbMessage.setBytes(packet.size());
@@ -1184,7 +1211,7 @@ static void startDoResolve(void *p)
        addCMsgSrcAddr(&msgh, cbuf, &dc->d_local, 0);
       }
       if(sendmsg(dc->d_socket, &msgh, 0) < 0 && g_logCommonErrors) 
-        L<<Logger::Warning<<"Sending UDP reply to client "<<dc->d_remote.toStringWithPort()<<" failed with: "<<strerror(errno)<<endl;
+        L<<Logger::Warning<<"Sending UDP reply to client "<<dc->getRemote()<<" failed with: "<<strerror(errno)<<endl;
       if(!SyncRes::s_nopacketcache && !variableAnswer && !sr.wasVariable() ) {
         t_packetCache->insertResponsePacket(dc->d_tag, dc->d_qhash, dc->d_mdp.d_qname, dc->d_mdp.d_qtype, dc->d_mdp.d_qclass,
                                             string((const char*)&*packet.begin(), packet.size()),
@@ -1353,48 +1380,75 @@ static void makeControlChannelSocket(int processNum=-1)
   }
 }
 
-static bool getQNameAndSubnet(const std::string& question, DNSName* dnsname, uint16_t* qtype, uint16_t* qclass, EDNSSubnetOpts* ednssubnet, std::map<uint16_t, EDNSOptionView>* options)
+static void getQNameAndSubnet(const std::string& question, DNSName* dnsname, uint16_t* qtype, uint16_t* qclass,
+                              bool& foundECS, EDNSSubnetOpts* ednssubnet, std::map<uint16_t, EDNSOptionView>* options,
+                              bool& foundXPF, ComboAddress* xpfSource, ComboAddress* xpfDest)
 {
-  bool found = false;
-  const struct dnsheader* dh = (struct dnsheader*)question.c_str();
+  const bool lookForXPF = xpfSource != nullptr;
+  const bool lookForECS = ednssubnet != nullptr;
+  const struct dnsheader* dh = reinterpret_cast<const struct dnsheader*>(question.c_str());
   size_t questionLen = question.length();
   unsigned int consumed=0;
   *dnsname=DNSName(question.c_str(), questionLen, sizeof(dnsheader), false, qtype, qclass, &consumed);
 
   size_t pos= sizeof(dnsheader)+consumed+4;
-  /* at least OPT root label (1), type (2), class (2) and ttl (4) + OPT RR rdlen (2)
-     = 11 */
-  if(ntohs(dh->arcount) == 1 && questionLen > pos + 11) { // this code can extract one (1) EDNS Subnet option
+  const size_t headerSize = /* root */ 1 + sizeof(dnsrecordheader);
+  const uint16_t arcount = ntohs(dh->arcount);
+
+  for (uint16_t arpos = 0; arpos < arcount && questionLen > (pos + headerSize) && ((lookForECS && !foundECS) || (lookForXPF && !foundXPF)); arpos++) {
+    if (question.at(pos) != 0) {
+      /* not an OPT or a XPF, bye. */
+      return;
+    }
+
+    pos += 1;
+    const dnsrecordheader* drh = reinterpret_cast<const dnsrecordheader*>(&question.at(pos));
+    pos += sizeof(dnsrecordheader);
+
+    if (pos >= questionLen) {
+      return;
+    }
+
     /* OPT root label (1) followed by type (2) */
-    if(question.at(pos)==0 && question.at(pos+1)==0 && question.at(pos+2)==QType::OPT) {
+    if(lookForECS && ntohs(drh->d_type) == QType::OPT) {
       if (!options) {
         char* ecsStart = nullptr;
         size_t ecsLen = 0;
-        int res = getEDNSOption((char*)question.c_str()+pos+9, questionLen - pos - 9, EDNSOptionCode::ECS, &ecsStart, &ecsLen);
+        /* we need to pass the record len */
+        int res = getEDNSOption(const_cast<char*>(reinterpret_cast<const char*>(&question.at(pos - sizeof(drh->d_clen)))), questionLen - pos + sizeof(drh->d_clen), EDNSOptionCode::ECS, &ecsStart, &ecsLen);
         if (res == 0 && ecsLen > 4) {
           EDNSSubnetOpts eso;
           if(getEDNSSubnetOptsFromString(ecsStart + 4, ecsLen - 4, &eso)) {
             *ednssubnet=eso;
-            found = true;
+            foundECS = true;
           }
         }
       }
       else {
-        int res = getEDNSOptions((char*)question.c_str()+pos+9, questionLen - pos - 9, *options);
+        /* we need to pass the record len */
+        int res = getEDNSOptions(reinterpret_cast<const char*>(&question.at(pos -sizeof(drh->d_clen))), questionLen - pos + (sizeof(drh->d_clen)), *options);
         if (res == 0) {
           const auto& it = options->find(EDNSOptionCode::ECS);
           if (it != options->end() && it->second.content != nullptr && it->second.size > 0) {
             EDNSSubnetOpts eso;
             if(getEDNSSubnetOptsFromString(it->second.content, it->second.size, &eso)) {
               *ednssubnet=eso;
-              found = true;
+              foundECS = true;
             }
           }
         }
       }
     }
+    else if (lookForXPF && ntohs(drh->d_type) == QType::XPF && ntohs(drh->d_class) == QClass::IN && drh->d_ttl == 0) {
+      if ((questionLen - pos) < ntohs(drh->d_clen)) {
+        return;
+      }
+
+      foundXPF = parseXPFPayload(reinterpret_cast<const char*>(&question.at(pos)), ntohs(drh->d_clen), *xpfSource, xpfDest);
+    }
+
+    pos += ntohs(drh->d_clen);
   }
-  return found;
 }
 
 static void handleRunningTCPQuestion(int fd, FDMultiplexer::funcparam_t& var)
@@ -1424,7 +1478,7 @@ static void handleRunningTCPQuestion(int fd, FDMultiplexer::funcparam_t& var)
     }
     if(!bytes || bytes < 0) {
       if(g_logCommonErrors)
-        L<<Logger::Error<<"TCP client "<< conn->d_remote.toString() <<" disconnected after first byte"<<endl;
+        L<<Logger::Error<<"TCP client "<< conn->d_remote.toStringWithPort() <<" disconnected after first byte"<<endl;
       t_fdm->removeReadFD(fd);
       return;
     }
@@ -1432,7 +1486,7 @@ static void handleRunningTCPQuestion(int fd, FDMultiplexer::funcparam_t& var)
   else if(conn->state==TCPConnection::GETQUESTION) {
     ssize_t bytes=recv(conn->getFD(), conn->data + conn->bytesread, conn->qlen - conn->bytesread, 0);
     if(!bytes || bytes < 0 || bytes > std::numeric_limits<std::uint16_t>::max()) {
-      L<<Logger::Error<<"TCP client "<< conn->d_remote.toString() <<" disconnected while reading question body"<<endl;
+      L<<Logger::Error<<"TCP client "<< conn->d_remote.toStringWithPort() <<" disconnected while reading question body"<<endl;
       t_fdm->removeReadFD(fd);
       return;
     }
@@ -1447,23 +1501,26 @@ static void handleRunningTCPQuestion(int fd, FDMultiplexer::funcparam_t& var)
       catch(MOADNSException &mde) {
         g_stats.clientParseError++;
         if(g_logCommonErrors)
-          L<<Logger::Error<<"Unable to parse packet from TCP client "<< conn->d_remote.toString() <<endl;
+          L<<Logger::Error<<"Unable to parse packet from TCP client "<< conn->d_remote.toStringWithPort() <<endl;
         return;
       }
       dc->d_tcpConnection = conn; // carry the torch
       dc->setSocket(conn->getFD()); // this is the only time a copy is made of the actual fd
       dc->d_tcp=true;
-      dc->setRemote(&conn->d_remote);
+      dc->setRemote(conn->d_remote);
+      dc->setSource(conn->d_remote);
       ComboAddress dest;
       memset(&dest, 0, sizeof(dest));
       dest.sin4.sin_family = conn->d_remote.sin4.sin_family;
       socklen_t len = dest.getSocklen();
       getsockname(conn->getFD(), (sockaddr*)&dest, &len); // if this fails, we're ok with it
       dc->setLocal(dest);
+      dc->setDestination(dest);
       DNSName qname;
       uint16_t qtype=0;
       uint16_t qclass=0;
       bool needECS = false;
+      bool needXPF = g_XPFAcl.match(conn->d_remote);
       string requestorId;
       string deviceId;
 #ifdef HAVE_PROTOBUF
@@ -1473,16 +1530,20 @@ static void handleRunningTCPQuestion(int fd, FDMultiplexer::funcparam_t& var)
       }
 #endif
 
-      if(needECS || (t_pdl && t_pdl->d_gettag)) {
+      if(needECS || needXPF || (t_pdl && t_pdl->d_gettag)) {
 
         try {
           std::map<uint16_t, EDNSOptionView> ednsOptions;
+          bool xpfFound = false;
           dc->d_ecsParsed = true;
-          dc->d_ecsFound = getQNameAndSubnet(std::string(conn->data, conn->qlen), &qname, &qtype, &qclass, &dc->d_ednssubnet, g_gettagNeedsEDNSOptions ? &ednsOptions : nullptr);
+          dc->d_ecsFound = false;
+          getQNameAndSubnet(std::string(conn->data, conn->qlen), &qname, &qtype, &qclass,
+                            dc->d_ecsFound, &dc->d_ednssubnet, g_gettagNeedsEDNSOptions ? &ednsOptions : nullptr,
+                            xpfFound, needXPF ? &dc->d_source : nullptr, needXPF ? &dc->d_destination : nullptr);
 
           if(t_pdl && t_pdl->d_gettag) {
             try {
-              dc->d_tag = t_pdl->gettag(conn->d_remote, dc->d_ednssubnet.source, dest, qname, qtype, &dc->d_policyTags, dc->d_data, ednsOptions, true, requestorId, deviceId);
+              dc->d_tag = t_pdl->gettag(dc->d_source, dc->d_ednssubnet.source, dc->d_destination, qname, qtype, &dc->d_policyTags, dc->d_data, ednsOptions, true, requestorId, deviceId);
             }
             catch(std::exception& e)  {
               if(g_logCommonErrors)
@@ -1508,7 +1569,7 @@ static void handleRunningTCPQuestion(int fd, FDMultiplexer::funcparam_t& var)
           const struct dnsheader* dh = (const struct dnsheader*) conn->data;
 
           if (!luaconfsLocal->protobufTaggedOnly) {
-            protobufLogQuery(luaconfsLocal->protobufServer, luaconfsLocal->protobufMaskV4, luaconfsLocal->protobufMaskV6, dc->d_uuid, conn->d_remote, dest, dc->d_ednssubnet.source, true, dh->id, conn->qlen, qname, qtype, qclass, dc->d_policyTags, dc->d_requestorId, dc->d_deviceId);
+            protobufLogQuery(luaconfsLocal->protobufServer, luaconfsLocal->protobufMaskV4, luaconfsLocal->protobufMaskV6, dc->d_uuid, dc->d_source, dc->d_destination, dc->d_ednssubnet.source, true, dh->id, conn->qlen, qname, qtype, qclass, dc->d_policyTags, dc->d_requestorId, dc->d_deviceId);
           }
         }
         catch(std::exception& e) {
@@ -1520,13 +1581,13 @@ static void handleRunningTCPQuestion(int fd, FDMultiplexer::funcparam_t& var)
       if(dc->d_mdp.d_header.qr) {
         delete dc;
         g_stats.ignoredCount++;
-        L<<Logger::Error<<"Ignoring answer from TCP client "<< conn->d_remote.toString() <<" on server socket!"<<endl;
+        L<<Logger::Error<<"Ignoring answer from TCP client "<< dc->getRemote() <<" on server socket!"<<endl;
         return;
       }
       if(dc->d_mdp.d_header.opcode) {
         delete dc;
         g_stats.ignoredCount++;
-        L<<Logger::Error<<"Ignoring non-query opcode from TCP client "<< conn->d_remote.toString() <<" on server socket!"<<endl;
+        L<<Logger::Error<<"Ignoring non-query opcode from TCP client "<< dc->getRemote() <<" on server socket!"<<endl;
         return;
       }
       else {
@@ -1615,8 +1676,11 @@ static string* doProcessUDPQuestion(const std::string& question, const ComboAddr
   unsigned int ctag=0;
   uint32_t qhash = 0;
   bool needECS = false;
+  bool needXPF = g_XPFAcl.match(fromaddr);
   std::vector<std::string> policyTags;
   LuaContext::LuaObject data;
+  ComboAddress source = fromaddr;
+  ComboAddress destination = destaddr;
   string requestorId;
   string deviceId;
 #ifdef HAVE_PROTOBUF
@@ -1650,16 +1714,23 @@ static string* doProcessUDPQuestion(const std::string& question, const ComboAddr
     */
 #endif
 
-    if(needECS || (t_pdl && t_pdl->d_gettag)) {
+    if(needECS || needXPF || (t_pdl && t_pdl->d_gettag)) {
       try {
         std::map<uint16_t, EDNSOptionView> ednsOptions;
-        ecsFound = getQNameAndSubnet(question, &qname, &qtype, &qclass, &ednssubnet, g_gettagNeedsEDNSOptions ? &ednsOptions : nullptr);
+        bool xpfFound = false;
+
+        ecsFound = false;
+
+        getQNameAndSubnet(question, &qname, &qtype, &qclass,
+                          ecsFound, &ednssubnet, g_gettagNeedsEDNSOptions ? &ednsOptions : nullptr,
+                          xpfFound, needXPF ? &source : nullptr, needXPF ? &destination : nullptr);
+
         qnameParsed = true;
         ecsParsed = true;
 
         if(t_pdl && t_pdl->d_gettag) {
           try {
-            ctag=t_pdl->gettag(fromaddr, ednssubnet.source, destaddr, qname, qtype, &policyTags, data, ednsOptions, false, requestorId, deviceId);
+            ctag=t_pdl->gettag(source, ednssubnet.source, destination, qname, qtype, &policyTags, data, ednsOptions, false, requestorId, deviceId);
           }
           catch(std::exception& e)  {
             if(g_logCommonErrors)
@@ -1679,7 +1750,7 @@ static string* doProcessUDPQuestion(const std::string& question, const ComboAddr
 #ifdef HAVE_PROTOBUF
     if(luaconfsLocal->protobufServer) {
       if (!luaconfsLocal->protobufTaggedOnly || !policyTags.empty()) {
-        protobufLogQuery(luaconfsLocal->protobufServer, luaconfsLocal->protobufMaskV4, luaconfsLocal->protobufMaskV6, uniqueId, fromaddr, destaddr, ednssubnet.source, false, dh->id, question.size(), qname, qtype, qclass, policyTags, requestorId, deviceId);
+        protobufLogQuery(luaconfsLocal->protobufServer, luaconfsLocal->protobufMaskV4, luaconfsLocal->protobufMaskV6, uniqueId, source, destination, ednssubnet.source, false, dh->id, question.size(), qname, qtype, qclass, policyTags, requestorId, deviceId);
       }
     }
 #endif /* HAVE_PROTOBUF */
@@ -1694,9 +1765,9 @@ static string* doProcessUDPQuestion(const std::string& question, const ComboAddr
     if (cacheHit) {
 #ifdef HAVE_PROTOBUF
       if(luaconfsLocal->protobufServer && (!luaconfsLocal->protobufTaggedOnly || !pbMessage.getAppliedPolicy().empty() || !pbMessage.getPolicyTags().empty())) {
-        Netmask requestorNM(fromaddr, fromaddr.sin4.sin_family == AF_INET ? luaconfsLocal->protobufMaskV4 : luaconfsLocal->protobufMaskV6);
+        Netmask requestorNM(source, source.sin4.sin_family == AF_INET ? luaconfsLocal->protobufMaskV4 : luaconfsLocal->protobufMaskV6);
         const ComboAddress& requestor = requestorNM.getMaskedNetwork();
-        pbMessage.update(uniqueId, &requestor, &destaddr, false, dh->id);
+        pbMessage.update(uniqueId, &requestor, &destination, false, dh->id);
         pbMessage.setEDNSSubnet(ednssubnet.source, ednssubnet.source.isIpv4() ? luaconfsLocal->protobufMaskV4 : luaconfsLocal->protobufMaskV6);
         pbMessage.setQueryTime(g_now.tv_sec, g_now.tv_usec);
         pbMessage.setRequestorId(requestorId);
@@ -1705,7 +1776,7 @@ static string* doProcessUDPQuestion(const std::string& question, const ComboAddr
       }
 #endif /* HAVE_PROTOBUF */
       if(!g_quiet)
-        L<<Logger::Notice<<t_id<< " question answered from packet cache tag="<<ctag<<" from "<<fromaddr.toString()<<endl;
+        L<<Logger::Notice<<t_id<< " question answered from packet cache tag="<<ctag<<" from "<<source.toStringWithPort()<<(source != fromaddr ? " (via "+fromaddr.toStringWithPort()+")" : "")<<endl;
 
       g_stats.packetCacheHits++;
       SyncRes::s_queries++;
@@ -1720,12 +1791,12 @@ static string* doProcessUDPQuestion(const std::string& question, const ComboAddr
        addCMsgSrcAddr(&msgh, cbuf, &destaddr, 0);
       }
       if(sendmsg(fd, &msgh, 0) < 0 && g_logCommonErrors)
-        L<<Logger::Warning<<"Sending UDP reply to client "<<fromaddr.toStringWithPort()<<" failed with: "<<strerror(errno)<<endl;
+        L<<Logger::Warning<<"Sending UDP reply to client "<<source.toStringWithPort()<<(source != fromaddr ? " (via "+fromaddr.toStringWithPort()+")" : "")<<" failed with: "<<strerror(errno)<<endl;
 
       if(response.length() >= sizeof(struct dnsheader)) {
         struct dnsheader tmpdh;
         memcpy(&tmpdh, response.c_str(), sizeof(tmpdh));
-        updateResponseStats(tmpdh.rcode, fromaddr, response.length(), 0, 0);
+        updateResponseStats(tmpdh.rcode, source, response.length(), 0, 0);
       }
       g_stats.avgLatencyUsec=(1-1.0/g_latencyStatSize)*g_stats.avgLatencyUsec + 0.0; // we assume 0 usec
       g_stats.avgLatencyOursUsec=(1-1.0/g_latencyStatSize)*g_stats.avgLatencyOursUsec + 0.0; // we assume 0 usec
@@ -1738,9 +1809,9 @@ static string* doProcessUDPQuestion(const std::string& question, const ComboAddr
   }
 
   if(t_pdl) {
-    if(t_pdl->ipfilter(fromaddr, destaddr, *dh)) {
+    if(t_pdl->ipfilter(source, destination, *dh)) {
       if(!g_quiet)
-       L<<Logger::Notice<<t_id<<" ["<<MT->getTid()<<"/"<<MT->numProcesses()<<"] DROPPED question from "<<fromaddr.toStringWithPort()<<" based on policy"<<endl;
+       L<<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;
     }
@@ -1748,7 +1819,7 @@ static string* doProcessUDPQuestion(const std::string& question, const ComboAddr
 
   if(MT->numProcesses() > g_maxMThreads) {
     if(!g_quiet)
-      L<<Logger::Notice<<t_id<<" ["<<MT->getTid()<<"/"<<MT->numProcesses()<<"] DROPPED question from "<<fromaddr.toStringWithPort()<<", over capacity"<<endl;
+      L<<Logger::Notice<<t_id<<" ["<<MT->getTid()<<"/"<<MT->numProcesses()<<"] DROPPED question from "<<source.toStringWithPort()<<(source != fromaddr ? " (via "+fromaddr.toStringWithPort()+")" : "")<<", over capacity"<<endl;
 
     g_stats.overCapacityDrops++;
     return 0;
@@ -1759,8 +1830,10 @@ static string* doProcessUDPQuestion(const std::string& question, const ComboAddr
   dc->d_tag=ctag;
   dc->d_qhash=qhash;
   dc->d_query = question;
-  dc->setRemote(&fromaddr);
+  dc->setRemote(fromaddr);
+  dc->setSource(source);
   dc->setLocal(destaddr);
+  dc->setDestination(destination);
   dc->d_tcp=false;
   dc->d_policyTags = policyTags;
   dc->d_data = data;
@@ -2990,6 +3063,8 @@ static int serviceMain(int argc, char*argv[])
   SyncRes::parseEDNSSubnetAddFor(::arg()["ecs-add-for"]);
   g_useIncomingECS = ::arg().mustDo("use-incoming-edns-subnet");
 
+  g_XPFAcl.toMasks(::arg()["xpf-allow-from"]);
+
   g_networkTimeoutMsec = ::arg().asNum("network-timeout");
 
   g_initialDomainMap = parseAuthAndForwards();
@@ -3242,7 +3317,7 @@ try
       for(expired_t::iterator i=expired.begin() ; i != expired.end(); ++i) {
         shared_ptr<TCPConnection> conn=any_cast<shared_ptr<TCPConnection> >(i->second);
         if(g_logCommonErrors)
-          L<<Logger::Warning<<"Timeout from remote TCP client "<< conn->d_remote.toString() <<endl;
+          L<<Logger::Warning<<"Timeout from remote TCP client "<< conn->d_remote.toStringWithPort() <<endl;
         t_fdm->removeReadFD(i->first);
       }
     }
@@ -3422,6 +3497,8 @@ int main(int argc, char **argv)
 
     ::arg().setSwitch("log-rpz-changes", "Log additions and removals to RPZ zones at Info level")="no";
 
+    ::arg().set("xpf-allow-from","XPF information is only processed from these subnets")="";
+
     ::arg().setCmd("help","Provide a helpful message");
     ::arg().setCmd("version","Print version string");
     ::arg().setCmd("config","Output blank configuration");
index 8c2070ae22111a8c98bd5e17249e43a015a1d0b9..4770fd6f341b360a8d194ed9b04861d6e11b8a85 100644 (file)
@@ -164,6 +164,7 @@ pdns_recursor_SOURCES = \
        webserver.cc webserver.hh \
        ws-api.cc ws-api.hh \
        ws-recursor.cc ws-recursor.hh \
+       xpf.cc xpf.hh \
        zoneparser-tng.cc zoneparser-tng.hh
 
 if !HAVE_LUA_HPP
@@ -248,11 +249,13 @@ testrunner_SOURCES = \
        test-signers.cc \
        test-syncres_cc.cc \
        test-tsig.cc \
+       test-xpf_cc.cc \
        testrunner.cc \
        tsigverifier.cc tsigverifier.hh \
        unix_utility.cc \
        validate.cc validate.hh \
        validate-recursor.cc validate-recursor.hh \
+       xpf.cc xpf.hh \
        zoneparser-tng.cc zoneparser-tng.hh
 
 testrunner_LDFLAGS = \
index e7e3fb5256a16535c2f0e3734a30d9fe10eb06ad..13a1e6b44b3c73834793d73867ee1f279e1a6ec3 100644 (file)
@@ -1162,3 +1162,17 @@ TCP port where the webserver should listen on.
 -  Default: yes
 
 If a PID file should be written to `socket-dir`_
+
+``xpf-allow-from``
+-------------
+.. versionadded:: 4.1.0
+
+-  IP ranges, separated by commas
+-  Default: empty
+
+This is an experimental implementation of `draft-bellis-dnsop-xpf`.
+The server will trust XPF records found in queries sent from those netmasks (both IPv4 and IPv6),
+and will adjust queries' source and destination accordingly. This is especially useful when the recursor
+is placed behind a proxy like dnsdist.
+Note that the `allow-from`_ setting is still applied to the original source address, and thus access restriction
+should be done on the proxy.
index 2a4cab01edb88bf1781b45900da607567f03e145..fe12a962cca48c3d4e30caa85fbc2abae51fe042 100644 (file)
@@ -14,9 +14,6 @@
 #include "ednssubnet.hh"
 #include "iputils.hh"
 
-/* extract a specific EDNS0 option from a pointer on the beginning rdLen of the OPT RR */
-int getEDNSOption(char* optRR, size_t len, uint16_t wantedOption, char ** optionValue, size_t * optionValueSize);
-
 BOOST_AUTO_TEST_SUITE(ednsoptions_cc)
 
 static void getRawQueryWithECSAndCookie(const DNSName& name, const Netmask& ecs, const std::string& clientCookie, const std::string& serverCookie, std::vector<uint8_t>& query)
diff --git a/pdns/recursordist/test-xpf_cc.cc b/pdns/recursordist/test-xpf_cc.cc
new file mode 100644 (file)
index 0000000..61d079c
--- /dev/null
@@ -0,0 +1,180 @@
+#define BOOST_TEST_DYN_LINK
+#define BOOST_TEST_NO_MAIN
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+#include <boost/test/unit_test.hpp>
+
+#include "xpf.hh"
+
+BOOST_AUTO_TEST_SUITE(xpf_cc)
+
+BOOST_AUTO_TEST_CASE(test_generateXPFPayload) {
+
+  /* Mixing v4 with v6 should throw */
+  BOOST_CHECK_THROW(generateXPFPayload(false, ComboAddress("192.0.2.1"), ComboAddress("2001:db8::1")), std::runtime_error);
+  BOOST_CHECK_THROW(generateXPFPayload(false, ComboAddress("2001:db8::1"), ComboAddress("192.0.2.1")), std::runtime_error);
+
+  {
+    /* v4 payload over UDP */
+    ComboAddress source("192.0.2.1:53");
+    ComboAddress destination("192.0.2.2:65535");
+
+    auto payload = generateXPFPayload(false, source, destination);
+    BOOST_CHECK_EQUAL(payload.size(), 14);
+    BOOST_CHECK_EQUAL(payload.at(0), 4);
+    BOOST_CHECK_EQUAL(payload.at(1), 17);
+
+    ComboAddress parsedSource;
+    ComboAddress parsedDestination;
+    BOOST_CHECK(parseXPFPayload(payload.c_str(), payload.size(), parsedSource, &parsedDestination));
+    BOOST_CHECK_EQUAL(parsedSource.toStringWithPort(), source.toStringWithPort());
+    BOOST_CHECK_EQUAL(parsedDestination.toStringWithPort(), destination.toStringWithPort());
+  }
+
+  {
+    /* v4 payload over TCP */
+    ComboAddress source("192.0.2.1:53");
+    ComboAddress destination("192.0.2.2:65535");
+
+    auto payload = generateXPFPayload(true, source, destination);
+    BOOST_CHECK_EQUAL(payload.size(), 14);
+    BOOST_CHECK_EQUAL(payload.at(0), 4);
+    BOOST_CHECK_EQUAL(payload.at(1), 6);
+
+    ComboAddress parsedSource;
+    ComboAddress parsedDestination;
+    BOOST_CHECK(parseXPFPayload(payload.c_str(), payload.size(), parsedSource, &parsedDestination));
+    BOOST_CHECK_EQUAL(parsedSource.toStringWithPort(), source.toStringWithPort());
+    BOOST_CHECK_EQUAL(parsedDestination.toStringWithPort(), destination.toStringWithPort());
+  }
+
+  {
+    /* v6 payload over UDP */
+    ComboAddress source("[2001:db8::1]:42");
+    ComboAddress destination("[::1]:65535");
+
+    auto payload = generateXPFPayload(false, source, destination);
+    BOOST_CHECK_EQUAL(payload.size(), 38);
+    BOOST_CHECK_EQUAL(payload.at(0), 6);
+    BOOST_CHECK_EQUAL(payload.at(1), 17);
+
+    ComboAddress parsedSource;
+    ComboAddress parsedDestination;
+    BOOST_CHECK(parseXPFPayload(payload.c_str(), payload.size(), parsedSource, &parsedDestination));
+    BOOST_CHECK_EQUAL(parsedSource.toStringWithPort(), source.toStringWithPort());
+    BOOST_CHECK_EQUAL(parsedDestination.toStringWithPort(), destination.toStringWithPort());
+  }
+
+  {
+    /* v6 payload over TCP */
+    ComboAddress source("[2001:db8::1]:42");
+    ComboAddress destination("[::1]:65535");
+
+    auto payload = generateXPFPayload(true, source, destination);
+    BOOST_CHECK_EQUAL(payload.size(), 38);
+    BOOST_CHECK_EQUAL(payload.at(0), 6);
+    BOOST_CHECK_EQUAL(payload.at(1), 6);
+
+    ComboAddress parsedSource;
+    ComboAddress parsedDestination;
+    BOOST_CHECK(parseXPFPayload(payload.c_str(), payload.size(), parsedSource, &parsedDestination));
+    BOOST_CHECK_EQUAL(parsedSource.toStringWithPort(), source.toStringWithPort());
+    BOOST_CHECK_EQUAL(parsedDestination.toStringWithPort(), destination.toStringWithPort());
+  }
+
+}
+
+BOOST_AUTO_TEST_CASE(test_parseXPFPayload) {
+
+  /* invalid sizes */
+  {
+    ComboAddress source;
+    ComboAddress destination;
+
+    BOOST_CHECK_EQUAL(parseXPFPayload(nullptr, 0, source, &destination), false);
+    BOOST_CHECK_EQUAL(parseXPFPayload(nullptr, 13, source, &destination), false);
+    BOOST_CHECK_EQUAL(parseXPFPayload(nullptr, 15, source, &destination), false);
+    BOOST_CHECK_EQUAL(parseXPFPayload(nullptr, 37, source, &destination), false);
+    BOOST_CHECK_EQUAL(parseXPFPayload(nullptr, 39, source, &destination), false);
+  }
+
+
+  {
+    /* invalid protocol */
+    ComboAddress source("[2001:db8::1]:42");
+    ComboAddress destination("[::1]:65535");
+
+    auto payload = generateXPFPayload(true, source, destination);
+    /* set protocol to 0 */
+    payload.at(1) = 0;
+
+    ComboAddress parsedSource;
+    ComboAddress parsedDestination;
+    BOOST_CHECK_EQUAL(parseXPFPayload(payload.c_str(), payload.size(), parsedSource, &parsedDestination), false);
+  }
+
+  {
+    /* invalid version */
+    ComboAddress source("[2001:db8::1]:42");
+    ComboAddress destination("[::1]:65535");
+
+    auto payload = generateXPFPayload(true, source, destination);
+    /* set version to 0 */
+    payload.at(0) = 0;
+
+    ComboAddress parsedSource;
+    ComboAddress parsedDestination;
+    BOOST_CHECK_EQUAL(parseXPFPayload(payload.c_str(), payload.size(), parsedSource, &parsedDestination), false);
+  }
+
+  {
+    /* payload too short (v6 size with v4 payload) */
+    ComboAddress source("192.0.2.1:53");
+    ComboAddress destination("192.0.2.2:65535");
+
+
+    auto payload = generateXPFPayload(true, source, destination);
+    /* set version to 6 */
+    payload.at(0) = 6;
+
+    ComboAddress parsedSource;
+    ComboAddress parsedDestination;
+    BOOST_CHECK_EQUAL(parseXPFPayload(payload.c_str(), payload.size(), parsedSource, &parsedDestination), false);
+  }
+
+  {
+    /* payload too long (v6 size with v4 payload) */
+    ComboAddress source("[2001:db8::1]:42");
+    ComboAddress destination("[::1]:65535");
+
+
+    auto payload = generateXPFPayload(true, source, destination);
+    /* set version to 4 */
+    payload.at(0) = 4;
+
+    ComboAddress parsedSource;
+    ComboAddress parsedDestination;
+    BOOST_CHECK_EQUAL(parseXPFPayload(payload.c_str(), payload.size(), parsedSource, &parsedDestination), false);
+  }
+
+  {
+    /* v4 payload over UDP */
+    ComboAddress source("192.0.2.1:53");
+    ComboAddress destination("192.0.2.2:65535");
+
+    auto payload = generateXPFPayload(false, source, destination);
+    BOOST_CHECK_EQUAL(payload.size(), 14);
+    BOOST_CHECK_EQUAL(payload.at(0), 4);
+    BOOST_CHECK_EQUAL(payload.at(1), 17);
+
+    ComboAddress parsedSource;
+    BOOST_CHECK(parseXPFPayload(payload.c_str(), payload.size(), parsedSource, nullptr));
+    BOOST_CHECK_EQUAL(parsedSource.toStringWithPort(), source.toStringWithPort());
+  }
+
+}
+
+
+BOOST_AUTO_TEST_SUITE_END()
diff --git a/pdns/recursordist/xpf.cc b/pdns/recursordist/xpf.cc
new file mode 120000 (symlink)
index 0000000..98ab722
--- /dev/null
@@ -0,0 +1 @@
+../xpf.cc
\ No newline at end of file
diff --git a/pdns/recursordist/xpf.hh b/pdns/recursordist/xpf.hh
new file mode 120000 (symlink)
index 0000000..023c8c4
--- /dev/null
@@ -0,0 +1 @@
+../xpf.hh
\ No newline at end of file
diff --git a/pdns/xpf.cc b/pdns/xpf.cc
new file mode 100644 (file)
index 0000000..8b093e4
--- /dev/null
@@ -0,0 +1,120 @@
+/*
+ * This file is part of PowerDNS or dnsdist.
+ * Copyright -- PowerDNS.COM B.V. and its contributors
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of version 2 of the GNU General Public License as
+ * published by the Free Software Foundation.
+ *
+ * In addition, for the avoidance of any doubt, permission is granted to
+ * link this program with OpenSSL and to (re)distribute the binaries
+ * produced as the result of such linking.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "xpf.hh"
+
+std::string generateXPFPayload(bool tcp, const ComboAddress& remote, const ComboAddress& local)
+{
+  if (remote.sin4.sin_family != local.sin4.sin_family) {
+    throw std::runtime_error("The XPF local and remote addresses must be of the same family");
+  }
+
+  std::string ret;
+  const uint8_t version = remote.isIPv4() ? 4 : 6;
+  const uint8_t protocol = tcp ? 6 : 17;
+  const size_t addrSize = remote.isIPv4() ? sizeof(remote.sin4.sin_addr.s_addr) : sizeof(remote.sin6.sin6_addr.s6_addr);
+  const uint16_t remotePort = remote.sin4.sin_port;
+  const uint16_t localPort = local.sin4.sin_port;
+
+  ret.reserve(sizeof(version) + sizeof(protocol) + (addrSize * 2) + sizeof(remotePort) + sizeof(localPort));
+
+  ret.append(reinterpret_cast<const char*>(&version), sizeof(version));
+  ret.append(reinterpret_cast<const char*>(&protocol), sizeof(protocol));
+
+  if (remote.isIPv4()) {
+    assert(addrSize == sizeof(remote.sin4.sin_addr.s_addr));
+    ret.append(reinterpret_cast<const char*>(&remote.sin4.sin_addr.s_addr), addrSize);
+  }
+  else {
+    assert(addrSize == sizeof(remote.sin6.sin6_addr.s6_addr));
+    ret.append(reinterpret_cast<const char*>(&remote.sin6.sin6_addr.s6_addr), addrSize);
+  }
+
+  if (remote.isIPv4()) {
+    assert(addrSize == sizeof(local.sin4.sin_addr.s_addr));
+    ret.append(reinterpret_cast<const char*>(&local.sin4.sin_addr.s_addr), addrSize);
+  }
+  else {
+    assert(addrSize == sizeof(local.sin6.sin6_addr.s6_addr));
+    ret.append(reinterpret_cast<const char*>(&local.sin6.sin6_addr.s6_addr), addrSize);
+  }
+
+  ret.append(reinterpret_cast<const char*>(&remotePort), sizeof(remotePort));
+  ret.append(reinterpret_cast<const char*>(&localPort), sizeof(localPort));
+
+  return ret;
+}
+
+bool parseXPFPayload(const char* payload, size_t len, ComboAddress& source, ComboAddress* destination)
+{
+  static const size_t addr4Size = sizeof(source.sin4.sin_addr.s_addr);
+  static const size_t addr6Size = sizeof(source.sin6.sin6_addr.s6_addr);
+  uint8_t version;
+  uint8_t protocol;
+  uint16_t sourcePort;
+  uint16_t destinationPort;
+
+  if (len != (sizeof(version) + sizeof(protocol) + (addr4Size * 2) + sizeof(sourcePort) + sizeof(destinationPort)) &&
+      len != (sizeof(version) + sizeof(protocol) + (addr6Size * 2) + sizeof(sourcePort) + sizeof(destinationPort))) {
+    return false;
+  }
+
+  size_t pos = 0;
+
+  memcpy(&version, payload + pos, sizeof(version));
+  pos += sizeof(version);
+
+  if (version != 4 && version != 6) {
+    return false;
+  }
+
+  memcpy(&protocol, payload + pos, sizeof(protocol));
+  pos += sizeof(protocol);
+
+  if (protocol != 6 && protocol != 17) {
+    return false;
+  }
+
+  const size_t addrSize = version == 4 ? sizeof(source.sin4.sin_addr.s_addr) : sizeof(source.sin6.sin6_addr.s6_addr);
+  if (len - pos != ((addrSize * 2) + sizeof(sourcePort) + sizeof(destinationPort))) {
+    return false;
+  }
+
+  source = makeComboAddressFromRaw(version, payload + pos, addrSize);
+  pos += addrSize;
+  if (destination != nullptr) {
+    *destination = makeComboAddressFromRaw(version, payload + pos, addrSize);
+  }
+  pos += addrSize;
+
+  memcpy(&sourcePort, payload + pos, sizeof(sourcePort));
+  pos += sizeof(sourcePort);
+  source.sin4.sin_port = sourcePort;
+
+  memcpy(&destinationPort, payload + pos, sizeof(destinationPort));
+  pos += sizeof(destinationPort);
+  if (destination != nullptr) {
+    destination->sin4.sin_port = destinationPort;
+  }
+
+  return true;
+}
diff --git a/pdns/xpf.hh b/pdns/xpf.hh
new file mode 100644 (file)
index 0000000..e3e18c3
--- /dev/null
@@ -0,0 +1,29 @@
+/*
+ * This file is part of PowerDNS or dnsdist.
+ * Copyright -- PowerDNS.COM B.V. and its contributors
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of version 2 of the GNU General Public License as
+ * published by the Free Software Foundation.
+ *
+ * In addition, for the avoidance of any doubt, permission is granted to
+ * link this program with OpenSSL and to (re)distribute the binaries
+ * produced as the result of such linking.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#pragma once
+
+#include <iputils.hh>
+
+std::string generateXPFPayload(bool tcp, const ComboAddress& remote, const ComboAddress& local);
+bool parseXPFPayload(const char* payload, size_t len, ComboAddress& source, ComboAddress* destination);
+
diff --git a/regression-tests.dnsdist/test_XPF.py b/regression-tests.dnsdist/test_XPF.py
new file mode 100644 (file)
index 0000000..261c517
--- /dev/null
@@ -0,0 +1,76 @@
+#!/usr/bin/env python
+
+import dns
+from dnsdisttests import DNSDistTest
+
+class XPFTest(DNSDistTest):
+    """
+    dnsdist is configured to add XPF to the query
+    """
+
+    _xpfCode = 65422
+    _config_template = """
+    newServer{address="127.0.0.1:%s", addXPF=true}
+    """
+
+    def checkMessageHasXPF(self, msg, expectedValue):
+        self.assertGreaterEqual(len(msg.additional), 1)
+
+        found = False
+        for add in msg.additional:
+            if add.rdtype == self._xpfCode:
+                found = True
+                self.assertEquals(add.rdclass, dns.rdataclass.IN)
+                self.assertEquals(add.ttl, 0)
+                xpfData = add.to_rdataset()[0].to_text()
+                # skip the ports
+                self.assertEquals(xpfData[:26], expectedValue[:26])
+
+        self.assertTrue(found)
+
+    def testXPF(self):
+        """
+        XPF
+        """
+        name = 'xpf.tests.powerdns.com.'
+        query = dns.message.make_query(name, 'A', 'IN')
+
+        expectedQuery = dns.message.make_query(name, 'A', 'IN')
+        # 0x04 is IPv4, 0x11 (17) is UDP then 127.0.0.1 as source and destination
+        # and finally the ports, zeroed because we have no way to know them beforehand
+        xpfData = "\# 14 04117f0000017f00000100000000"
+        rdata = dns.rdata.from_text(dns.rdataclass.IN, self._xpfCode, xpfData)
+        rrset = dns.rrset.from_rdata(name, 60, rdata)
+        expectedQuery.additional.append(rrset)
+
+        response = dns.message.make_response(expectedQuery)
+
+        (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response)
+        self.assertTrue(receivedQuery)
+        self.assertTrue(receivedResponse)
+        receivedQuery.id = expectedQuery.id
+        receivedResponse.id = response.id
+
+        self.assertEquals(receivedQuery, expectedQuery)
+        self.checkMessageHasXPF(receivedQuery, xpfData)
+        self.assertEquals(response, receivedResponse)
+
+        expectedQuery = dns.message.make_query(name, 'A', 'IN')
+        # 0x04 is IPv4, 0x06 (6) is TCP then 127.0.0.1 as source and destination
+        # and finally the ports, zeroed because we have no way to know them beforehand
+        xpfData = "\# 14 04067f0000017f00000100000000"
+        rdata = dns.rdata.from_text(dns.rdataclass.IN, self._xpfCode, xpfData)
+        rrset = dns.rrset.from_rdata(name, 60, rdata)
+        expectedQuery.additional.append(rrset)
+
+        response = dns.message.make_response(expectedQuery)
+
+        (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response)
+        self.assertTrue(receivedQuery)
+        self.assertTrue(receivedResponse)
+        receivedQuery.id = expectedQuery.id
+        receivedResponse.id = response.id
+
+        self.assertEquals(receivedQuery, expectedQuery)
+        self.checkMessageHasXPF(receivedQuery, xpfData)
+        self.assertEquals(response, receivedResponse)