]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
Merge pull request #8946 from omoerbeek/rec-buildbot-test
authorOtto Moerbeek <otto.moerbeek@open-xchange.com>
Wed, 18 Mar 2020 14:33:33 +0000 (15:33 +0100)
committerGitHub <noreply@github.com>
Wed, 18 Mar 2020 14:33:33 +0000 (15:33 +0100)
rec: test now uses rec_control, so supply location of the executable

54 files changed:
.github/workflows/fuzz.yml
docs/changelog/4.3.rst
docs/manpages/sdig.1.rst
docs/secpoll.zone
fuzzing/README.md
fuzzing/corpus/proxy-protocol-raw-packets/proxy-protocol-local-header [new file with mode: 0644]
fuzzing/corpus/proxy-protocol-raw-packets/proxy-protocol-v4-with-tlvs [new file with mode: 0644]
pdns/.gitignore
pdns/Makefile.am
pdns/dnsdist-console.cc
pdns/dnsdist-lua-actions.cc
pdns/dnsdist-lua-bindings-dnsquestion.cc
pdns/dnsdist-lua.cc
pdns/dnsdist-tcp.cc
pdns/dnsdist.cc
pdns/dnsdist.hh
pdns/dnsdistdist/Makefile.am
pdns/dnsdistdist/dnsdist-healthchecks.cc
pdns/dnsdistdist/dnsdist-idstate.cc
pdns/dnsdistdist/dnsdist-proxy-protocol.cc [new file with mode: 0644]
pdns/dnsdistdist/dnsdist-proxy-protocol.hh [new file with mode: 0644]
pdns/dnsdistdist/docs/reference/config.rst
pdns/dnsdistdist/docs/reference/dq.rst
pdns/dnsdistdist/docs/rules-actions.rst
pdns/dnsdistdist/doh.cc
pdns/dnsdistdist/proxy-protocol.cc [new symlink]
pdns/dnsdistdist/proxy-protocol.hh [new symlink]
pdns/dnsdistdist/test-proxy_protocol_cc.cc [new symlink]
pdns/fuzz_proxyprotocol.cc [new file with mode: 0644]
pdns/lua-recursor4-ffi.hh
pdns/lua-recursor4.cc
pdns/lua-recursor4.hh
pdns/pdns_recursor.cc
pdns/proxy-protocol.cc [new file with mode: 0644]
pdns/proxy-protocol.hh [new file with mode: 0644]
pdns/rec-snmp.cc
pdns/rec_channel_rec.cc
pdns/recursordist/Makefile.am
pdns/recursordist/RECURSOR-MIB.txt
pdns/recursordist/docs/lua-scripting/dq.rst
pdns/recursordist/docs/lua-scripting/hooks.rst
pdns/recursordist/docs/metrics.rst
pdns/recursordist/docs/settings.rst
pdns/recursordist/proxy-protocol.cc [new symlink]
pdns/recursordist/proxy-protocol.hh [new symlink]
pdns/sdig.cc
pdns/syncres.hh
pdns/test-proxy_protocol_cc.cc [new file with mode: 0644]
regression-tests.common/proxyprotocol.py [new file with mode: 0644]
regression-tests.dnsdist/proxyprotocol.py [new symlink]
regression-tests.dnsdist/test_ProxyProtocol.py [new file with mode: 0644]
regression-tests.recursor-dnssec/proxyprotocol.py [new symlink]
regression-tests.recursor-dnssec/recursortests.py
regression-tests.recursor-dnssec/test_ProxyProtocol.py [new file with mode: 0644]

index 9083017e8581f82d28408bca930616c736515062..52a2f397f9f8ba3dbd09e0dd1c5ad706e93d9cc9 100644 (file)
@@ -8,13 +8,13 @@ jobs:
       uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master
       with:
         oss-fuzz-project-name: 'powerdns'
-        dry-run: true
+        dry-run: false
     - name: Run Fuzzers
       uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master
       with:
         oss-fuzz-project-name: 'powerdns'
         fuzz-seconds: 600
-        dry-run: true
+        dry-run: false
     - name: Upload Crash
       uses: actions/upload-artifact@v1
       if: failure()
index f1e598c91e2b74d2e9deea95d116be45c8375ca1..b666d6d713fbaed98d14a31ce073ce99a9e0561b 100644 (file)
@@ -1,6 +1,25 @@
 Changelogs for 4.3.x
 ====================
 
+.. changelog::
+  :version: 4.3.0-rc2
+  :released: 18th of March 2020
+
+  This is the first Release Candidate for version 4.3.0 of the Authoritative Server.
+  The version called 4.3.0-rc1 was never released because of the cache cleanup change mentioned below.
+
+  .. change::
+    :tags: Improvements
+    :pullreq: 8924
+
+    Make sure we look at 10% of all cached items during cleanup (Kees Monshouwer)
+
+  .. change::
+    :tags: Bug Fixes
+    :pullreq: 8936
+
+    emit correct NSEC/NSEC3 bitmaps in hidden key situations (Robin Geuze)
+
 .. changelog::
   :version: 4.3.0-beta2
   :released: 21st of February 2020
index 8ce4e1c6821882d335af9d1a32049425d7fe1db2..a2a89d0a150e26878f203d7410d257e33adbb485 100644 (file)
@@ -31,13 +31,15 @@ hidesoadetails
     Don't show the SOA serial in the response.
 hidettl
     Replace TTLs with `[ttl]` in the response.
+proxy *TCP?* *SRC* *DST*
+    Wrap query in PROXYv2 protocol with these parameters. The first parameter accepts 0 for UDP and 1 for TCP. The second and third take IP addresses and port.
 recurse
     Set the RD bit in the question.
 showflags
     Show the NSEC3 flags in the response (they are hidden by default).
 tcp
     Use TCP instead of UDP to send the query.
-xpf *XPFCODE* *XPFVERSION* *XPFPROTO* *XPFSRC* *XPFSRC*
+xpf *XPFCODE* *XPFVERSION* *XPFPROTO* *XPFSRC* *XPFDST*
        Send an *XPF* additional with these parameters.
 
 Examples
@@ -50,4 +52,3 @@ Simple queries to local resolvers
 Query to a DNS-over-HTTPS server requesting dnssec and recursion
     sdig https://dns.somesample.net/dns-query 443 example.com A dnssec recurse
 
-
index a709c7782085ca103df02f333af4d8b231e132a0..dc16c95bf001058e1e7133b5a82fff0d9d46e185 100644 (file)
@@ -1,4 +1,4 @@
-@       86400   IN  SOA pdns-public-ns1.powerdns.com. pieter\.lexis.powerdns.com. 2020030304 10800 3600 604800 10800
+@       86400   IN  SOA pdns-public-ns1.powerdns.com. pieter\.lexis.powerdns.com. 2020031801 10800 3600 604800 10800
 @       3600    IN  NS  pdns-public-ns1.powerdns.com.
 @       3600    IN  NS  pdns-public-ns2.powerdns.com.
 
@@ -62,7 +62,9 @@ auth-4.2.0.security-status                              60 IN TXT "1 OK"
 auth-4.2.1.security-status                              60 IN TXT "1 OK"
 auth-4.3.0-alpha1.security-status                       60 IN TXT "2 Unsupported pre-release (no known vulnerabilities)"
 auth-4.3.0-beta1.security-status                        60 IN TXT "2 Unsupported pre-release (no known vulnerabilities)"
-auth-4.3.0-beta2.security-status                        60 IN TXT "1 OK"
+auth-4.3.0-beta2.security-status                        60 IN TXT "2 Unsupported pre-release (no known vulnerabilities)"
+auth-4.3.0-rc1.security-status                          60 IN TXT "2 Unsupported pre-release (no known vulnerabilities)"
+auth-4.3.0-rc2.security-status                          60 IN TXT "1 OK"
 
 ; Auth Debian
 auth-3.4.1-2.debian.security-status                     60 IN TXT "3 Upgrade now, see https://doc.powerdns.com/3/security/powerdns-advisory-2015-01/ and https://doc.powerdns.com/3/security/powerdns-advisory-2015-02/ and https://doc.powerdns.com/3/security/powerdns-advisory-2016-02/ and https://doc.powerdns.com/3/security/powerdns-advisory-2016-03/ and https://doc.powerdns.com/3/security/powerdns-advisory-2016-04/ and https://doc.powerdns.com/3/security/powerdns-advisory-2016-05/"
index c42c5c12f1539e898867ca35b3b244ea8ae72e2b..055c44883008d0b5e78b7a573a31c4c5417430db 100644 (file)
@@ -12,6 +12,7 @@ The current targets cover:
 - the auth, dnsdist and rec packet caches (fuzz_target_packetcache and
   fuzz_target_dnsdistcache) ;
 - MOADNSParser (fuzz_target_moadnsparser) ;
+- the Proxy Protocol parser (fuzz_target_proxyprotocol) ;
 - ZoneParserTNG (fuzz_target_zoneparsertng).
 
 By default the targets are linked against a standalone target,
@@ -38,7 +39,9 @@ Corpus
 This directory contains a few files used for continuous fuzzing
 of the PowerDNS products.
 
-The 'corpus' directory contains two sub-directories:
+The 'corpus' directory contains three sub-directories:
+- proxy-protocol-raw-packets/ contains DNS queries prefixed with a Proxy
+  Protocol v2 header, used by fuzz_target_proxyprotocol ;
 - raw-dns-packets/ contains DNS queries and responses as captured on
   the wire. These are used by the fuzz_target_dnsdistcache,
   fuzz_target_moadnsparser and fuzz_target_packetcache targets ;
diff --git a/fuzzing/corpus/proxy-protocol-raw-packets/proxy-protocol-local-header b/fuzzing/corpus/proxy-protocol-raw-packets/proxy-protocol-local-header
new file mode 100644 (file)
index 0000000..f2f8264
Binary files /dev/null and b/fuzzing/corpus/proxy-protocol-raw-packets/proxy-protocol-local-header differ
diff --git a/fuzzing/corpus/proxy-protocol-raw-packets/proxy-protocol-v4-with-tlvs b/fuzzing/corpus/proxy-protocol-raw-packets/proxy-protocol-v4-with-tlvs
new file mode 100644 (file)
index 0000000..250bed2
Binary files /dev/null and b/fuzzing/corpus/proxy-protocol-raw-packets/proxy-protocol-v4-with-tlvs differ
index c17b907b92653a0285c47e96bb37fe50f81078da..41e738c99468f0cabd7bb32aee551c938e000ca1 100644 (file)
@@ -65,4 +65,5 @@ effective_tld_names.dat
 /fuzz_target_dnsdistcache
 /fuzz_target_moadnsparser
 /fuzz_target_packetcache
+/fuzz_target_proxyprotocol
 /fuzz_target_zoneparsertng
index ab62112896dba3e3fc7e7eab02d26b6d6c05984e..524110a07be8a0035da66cc314e16456fd9b19b5 100644 (file)
@@ -486,6 +486,7 @@ sdig_SOURCES = \
        logger.cc \
        misc.cc misc.hh \
        nsecrecords.cc \
+       proxy-protocol.cc proxy-protocol.hh \
        qtype.cc \
        rcpgenerator.cc rcpgenerator.hh \
        sdig.cc \
@@ -1292,6 +1293,7 @@ testrunner_SOURCES = \
        nsecrecords.cc \
        opensslsigners.cc opensslsigners.hh \
        pollmplexer.cc \
+       proxy-protocol.cc proxy-protocol.hh \
        qtype.cc \
        rcpgenerator.cc \
        responsestats.cc \
@@ -1322,6 +1324,7 @@ testrunner_SOURCES = \
        test-nameserver_cc.cc \
        test-packetcache_cc.cc \
        test-packetcache_hh.cc \
+       test-proxy_protocol_cc.cc \
        test-rcpgenerator_cc.cc \
        test-signers.cc \
        test-sha_hh.cc \
@@ -1420,6 +1423,7 @@ fuzz_targets_programs =  \
        fuzz_target_dnsdistcache \
        fuzz_target_moadnsparser \
        fuzz_target_packetcache \
+       fuzz_target_proxyprotocol \
        fuzz_target_zoneparsertng
 
 fuzz_targets: $(fuzz_targets_programs)
@@ -1476,6 +1480,16 @@ fuzz_target_packetcache_DEPENDENCIES = $(fuzz_targets_deps)
 fuzz_target_packetcache_LDFLAGS = $(fuzz_targets_ldflags)
 fuzz_target_packetcache_LDADD = $(fuzz_targets_libs)
 
+fuzz_target_proxyprotocol_SOURCES = \
+       fuzz_proxyprotocol.cc \
+       iputils.hh \
+       proxy-protocol.cc \
+       proxy-protocol.hh
+
+fuzz_target_proxyprotocol_DEPENDENCIES = $(fuzz_targets_deps)
+fuzz_target_proxyprotocol_LDFLAGS = $(fuzz_targets_ldflags)
+fuzz_target_proxyprotocol_LDADD = $(fuzz_targets_libs)
+
 fuzz_target_dnsdistcache_SOURCES = \
        fuzz_dnsdistcache.cc \
        dnsdist-cache.cc dnsdist-cache.hh \
index 6fe525e2ddc4f71acc77fd576a2729a31180e443..b1ec405a10e9f1b104c983038ad118fd78d827ae 100644 (file)
@@ -576,6 +576,7 @@ const std::vector<ConsoleKeyword> g_consoleKeywords{
   { "showTLSErrorCounters", true, "", "show metrics about TLS handshake failures" },
   { "showVersion", true, "", "show the current version" },
   { "shutdown", true, "", "shut down `dnsdist`" },
+  { "SetProxyProtocolValuesAction", true, "values", "Set the Proxy-Protocol values for this queries to 'values'" },
   { "SkipCacheAction", true, "", "Don’t lookup the cache for this query, don’t store the answer" },
   { "SNIRule", true, "name", "Create a rule which matches on the incoming TLS SNI value, if any (DoT or DoH)" },
   { "snmpAgent", true, "enableTraps [, masterSocket]", "enable `SNMP` support. `enableTraps` is a boolean indicating whether traps should be sent and `masterSocket` an optional string specifying how to connect to the master agent"},
index 82ae55ffe344ad774302e6a65303964e4c1c6b5d..c3e1e11360f3e896d20cb0976470807177c405e8 100644 (file)
@@ -1418,6 +1418,37 @@ private:
   bool d_nxd;
 };
 
+class SetProxyProtocolValuesAction : public DNSAction
+{
+public:
+  SetProxyProtocolValuesAction(const std::vector<std::pair<uint8_t, std::string>>& values)
+  {
+    d_values.reserve(values.size());
+    for (const auto& value : values) {
+      d_values.push_back({value.second, value.first});
+    }
+  }
+
+  DNSAction::Action operator()(DNSQuestion* dq, std::string* ruleresult) const override
+  {
+    if (!dq->proxyProtocolValues) {
+      dq->proxyProtocolValues = make_unique<std::vector<ProxyProtocolValue>>();
+    }
+
+    *(dq->proxyProtocolValues) = d_values;
+
+    return Action::None;
+  }
+
+  std::string toString() const override
+  {
+    return "set Proxy-Protocol values";
+  }
+
+private:
+  std::vector<ProxyProtocolValue> d_values;
+};
+
 template<typename T, typename ActionT>
 static void addAction(GlobalStateHolder<vector<T> > *someRulActions, const luadnsrule_t& var, const std::shared_ptr<ActionT>& action, boost::optional<luaruleparams_t>& params) {
   setLuaSideEffect();
@@ -1819,4 +1850,8 @@ void setupLuaActions()
       parseResponseConfig(vars, action->d_responseConfig);
       return ret;
     });
+
+  g_lua.writeFunction("SetProxyProtocolValuesAction", [](const std::vector<std::pair<uint8_t, std::string>>& values) {
+      return std::shared_ptr<DNSAction>(new SetProxyProtocolValuesAction(values));
+    });
 }
index 71840e7f36481f79cc40cd4353102e3304c6d898..9c87d129c8f9b65c2893f8aed69493b6820810e4 100644 (file)
@@ -118,6 +118,18 @@ void setupLuaBindingsDNSQuestion()
       return *dq.qTag;
     });
 
+  g_lua.registerFunction<void(DNSQuestion::*)(std::vector<std::pair<uint8_t, std::string>>)>("setProxyProtocolValues", [](DNSQuestion& dq, const std::vector<std::pair<uint8_t, std::string>>& values) {
+      if (!dq.proxyProtocolValues) {
+        dq.proxyProtocolValues = make_unique<std::vector<ProxyProtocolValue>>();
+      }
+
+      dq.proxyProtocolValues->clear();
+      dq.proxyProtocolValues->reserve(values.size());
+      for (const auto& value : values) {
+        dq.proxyProtocolValues->push_back({value.second, value.first});
+      }
+    });
+
   /* LuaWrapper doesn't support inheritance */
   g_lua.registerMember<const ComboAddress (DNSResponse::*)>("localaddr", [](const DNSResponse& dq) -> const ComboAddress { return *dq.local; }, [](DNSResponse& dq, const ComboAddress newLocal) { (void) newLocal; });
   g_lua.registerMember<const DNSName (DNSResponse::*)>("qname", [](const DNSResponse& dq) -> const DNSName { return *dq.qname; }, [](DNSResponse& dq, const DNSName newName) { (void) newName; });
index 3adf375afd2455111c5c51c8bcfba58ee6dead09..1b4abfe68259b9eb4adb1dd0ff13e1a393249e8e 100644 (file)
@@ -437,6 +437,10 @@ static void setupLuaConfig(bool client, bool configCheck)
         ret->useECS=boost::get<bool>(vars["useClientSubnet"]);
       }
 
+      if(vars.count("useProxyProtocol")) {
+        ret->useProxyProtocol = boost::get<bool>(vars["useProxyProtocol"]);
+      }
+
       if(vars.count("disableZeroScope")) {
         ret->disableZeroScope=boost::get<bool>(vars["disableZeroScope"]);
       }
index 40f8ac2dc46b0bc7154a2f8ef7cb74c126560e77..4ae3199e086126eac2646308a77a22d5789a9fac 100644 (file)
@@ -21,6 +21,7 @@
  */
 #include "dnsdist.hh"
 #include "dnsdist-ecs.hh"
+#include "dnsdist-proxy-protocol.hh"
 #include "dnsdist-rings.hh"
 #include "dnsdist-xpf.hh"
 
@@ -172,6 +173,27 @@ public:
     return d_enableFastOpen;
   }
 
+  bool canBeReused() const
+  {
+    /* we can't reuse a connection where a proxy protocol payload has been sent,
+       since:
+       - it cannot be reused for a different client
+       - we might have different TLV values for each query
+    */
+    if (d_ds && d_ds->useProxyProtocol) {
+      return false;
+    }
+    return true;
+  }
+
+  bool matches(const std::shared_ptr<DownstreamState>& ds) const
+  {
+    if (!ds || !d_ds) {
+      return false;
+    }
+    return ds == d_ds;
+  }
+
 private:
   std::unique_ptr<Socket> d_socket{nullptr};
   std::shared_ptr<DownstreamState> d_ds{nullptr};
@@ -207,6 +229,11 @@ static void releaseDownstreamConnection(std::unique_ptr<TCPConnectionToBackend>&
     return;
   }
 
+  if (!conn->canBeReused()) {
+    conn.reset();
+    return;
+  }
+
   const auto& remote = conn->getRemote();
   const auto& it = t_downstreamConnections.find(remote);
   if (it != t_downstreamConnections.end()) {
@@ -653,6 +680,8 @@ public:
   bool d_isXFR{false};
   bool d_xfrStarted{false};
   bool d_selfGeneratedResponse{false};
+  bool d_proxyProtocolPayloadAdded{false};
+  bool d_proxyProtocolPayloadHasTLV{false};
 };
 
 static void handleIOCallback(int fd, FDMultiplexer::funcparam_t& param);
@@ -790,7 +819,6 @@ static void sendQueryToBackend(std::shared_ptr<IncomingTCPConnectionState>& stat
   state->d_state = IncomingTCPConnectionState::State::sendingQueryToBackend;
   state->d_currentPos = 0;
   state->d_firstResponsePacket = true;
-  state->d_downstreamConnection.reset();
 
   if (state->d_xfrStarted) {
     /* sorry, but we are not going to resume a XFR if we have already sent some packets
@@ -798,20 +826,29 @@ static void sendQueryToBackend(std::shared_ptr<IncomingTCPConnectionState>& stat
     return;
   }
 
-  if (state->d_downstreamFailures < state->d_ds->retries) {
-    try {
-      state->d_downstreamConnection = getConnectionToDownstream(ds, state->d_downstreamFailures, now);
+  if (!state->d_downstreamConnection) {
+    if (state->d_downstreamFailures < state->d_ds->retries) {
+      try {
+        state->d_downstreamConnection = getConnectionToDownstream(ds, state->d_downstreamFailures, now);
+      }
+      catch (const std::runtime_error& e) {
+        state->d_downstreamConnection.reset();
+      }
     }
-    catch (const std::runtime_error& e) {
-      state->d_downstreamConnection.reset();
+
+    if (!state->d_downstreamConnection) {
+      ++ds->tcpGaveUp;
+      ++state->d_ci.cs->tcpGaveUp;
+      vinfolog("Downstream connection to %s failed %d times in a row, giving up.", ds->getName(), state->d_downstreamFailures);
+      return;
     }
-  }
 
-  if (!state->d_downstreamConnection) {
-    ++ds->tcpGaveUp;
-    ++state->d_ci.cs->tcpGaveUp;
-    vinfolog("Downstream connection to %s failed %d times in a row, giving up.", ds->getName(), state->d_downstreamFailures);
-    return;
+    if (ds->useProxyProtocol && !state->d_proxyProtocolPayloadAdded) {
+      /* we know there is no TLV values to add, otherwise we would not have tried
+         to reuse the connection and d_proxyProtocolPayloadAdded would be true already */
+      addProxyProtocol(state->d_buffer, true, state->d_ci.remote, state->d_ids.origDest, std::vector<ProxyProtocolValue>());
+      state->d_proxyProtocolPayloadAdded = true;
+    }
   }
 
   vinfolog("Got query for %s|%s from %s (%s), relayed to %s", state->d_ids.qname.toLogString(), QType(state->d_ids.qtype).getName(), state->d_ci.remote.toStringWithPort(), (state->d_ci.cs->tlsFrontend ? "DoT" : "TCP"), ds->getName());
@@ -828,6 +865,7 @@ static void handleQuery(std::shared_ptr<IncomingTCPConnectionState>& state, stru
   }
 
   state->d_readingFirstQuery = false;
+  state->d_proxyProtocolPayloadAdded = false;
   ++state->d_queriesCount;
   ++state->d_ci.cs->queries;
   ++g_stats.queries;
@@ -905,7 +943,6 @@ static void handleQuery(std::shared_ptr<IncomingTCPConnectionState>& state, stru
     return;
   }
 
-  state->d_buffer.resize(dq.len);
   setIDStateFromDNSQuestion(state->d_ids, dq, std::move(qname));
 
   const uint8_t sizeBytes[] = { static_cast<uint8_t>(dq.len / 256), static_cast<uint8_t>(dq.len % 256) };
@@ -913,6 +950,30 @@ static void handleQuery(std::shared_ptr<IncomingTCPConnectionState>& state, stru
      that could occur if we had to deal with the size during the processing,
      especially alignment issues */
   state->d_buffer.insert(state->d_buffer.begin(), sizeBytes, sizeBytes + 2);
+  dq.len = dq.len + 2;
+  dq.dh = reinterpret_cast<dnsheader*>(&state->d_buffer.at(0));
+  dq.size = state->d_buffer.size();
+  state->d_buffer.resize(dq.len);
+
+  if (state->d_ds->useProxyProtocol) {
+    /* if we ever sent a TLV over a connection, we can never go back */
+    if (!state->d_proxyProtocolPayloadHasTLV) {
+      state->d_proxyProtocolPayloadHasTLV = dq.proxyProtocolValues && !dq.proxyProtocolValues->empty();
+    }
+
+    if (state->d_downstreamConnection && !state->d_proxyProtocolPayloadHasTLV && state->d_downstreamConnection->matches(state->d_ds)) {
+      /* we have an existing connection, on which we already sent a Proxy Protocol header with no values
+         (in the previous query had TLV values we would have reset the connection afterwards),
+         so let's reuse it as long as we still don't have any values */
+      state->d_proxyProtocolPayloadAdded = false;
+    }
+    else {
+      state->d_downstreamConnection.reset();
+      addProxyProtocol(state->d_buffer, true, state->d_ci.remote, state->d_ids.origDest, dq.proxyProtocolValues ? *dq.proxyProtocolValues : std::vector<ProxyProtocolValue>());
+      state->d_proxyProtocolPayloadAdded = true;
+    }
+  }
+
   sendQueryToBackend(state, now);
 }
 
@@ -1023,7 +1084,20 @@ static void handleDownstreamIO(std::shared_ptr<IncomingTCPConnectionState>& stat
           /* but don't reset it either, we will need to read more messages */
         }
         else {
-          releaseDownstreamConnection(std::move(state->d_downstreamConnection));
+          /* if we did not send a Proxy Protocol header, let's pool the connection */
+          if (state->d_ds && state->d_ds->useProxyProtocol == false) {
+            releaseDownstreamConnection(std::move(state->d_downstreamConnection));
+          }
+          else {
+            if (state->d_proxyProtocolPayloadHasTLV) {
+              /* sent a Proxy Protocol header with TLV values, we can't reuse it */
+              state->d_downstreamConnection.reset();
+            }
+            else {
+              /* if we did but there was no TLV values, let's try to reuse it but only
+                 for this incoming connection */
+            }
+          }
         }
         fd = -1;
 
@@ -1082,6 +1156,7 @@ static void handleDownstreamIO(std::shared_ptr<IncomingTCPConnectionState>& stat
   }
 
   if (connectionDied) {
+    state->d_downstreamConnection.reset();
     sendQueryToBackend(state, now);
   }
 }
index 5c5803b1c728cf6a0dacb81d5865202b11b5b7be..53637eb73f9b18fb324c45e50cf15fd2cf672f93 100644 (file)
@@ -48,6 +48,7 @@
 #include "dnsdist-ecs.hh"
 #include "dnsdist-healthchecks.hh"
 #include "dnsdist-lua.hh"
+#include "dnsdist-proxy-protocol.hh"
 #include "dnsdist-rings.hh"
 #include "dnsdist-secpoll.hh"
 #include "dnsdist-xpf.hh"
@@ -1367,6 +1368,10 @@ static void processUDPQuery(ClientState& cs, LocalHolders& holders, const struct
 
     dh->id = idOffset;
 
+    if (ss->useProxyProtocol) {
+      addProxyProtocol(dq);
+    }
+
     int fd = pickBackendSocketForSending(ss);
     ssize_t ret = udpClientSendRequestToBackend(ss, fd, query, dq.len);
 
index c5aff84584878a56c5f14f568907c5b7981fd123..3848149fc8d0c774a03c280ec69c905c4bb67f69 100644 (file)
@@ -49,6 +49,7 @@
 #include "sholder.hh"
 #include "tcpiohandler.hh"
 #include "uuid-utils.hh"
+#include "proxy-protocol.hh"
 
 void carbonDumpThread();
 uint64_t uptimeOfProcess(const std::string& str);
@@ -84,6 +85,7 @@ struct DNSQuestion
   const ComboAddress* local{nullptr};
   const ComboAddress* remote{nullptr};
   std::shared_ptr<QTag> qTag{nullptr};
+  std::unique_ptr<std::vector<ProxyProtocolValue>> proxyProtocolValues{nullptr};
   std::shared_ptr<std::map<uint16_t, EDNSOptionView> > ednsOptions;
   std::shared_ptr<DNSCryptQuery> dnsCryptQuery{nullptr};
   std::shared_ptr<DNSDistPacketCache> packetCache{nullptr};
@@ -832,6 +834,7 @@ struct DownstreamState
   bool mustResolve{false};
   bool upStatus{false};
   bool useECS{false};
+  bool useProxyProtocol{false};
   bool setCD{false};
   bool disableZeroScope{false};
   std::atomic<bool> connected{false};
index 54bbd0ae3f49398540abbf935c44fda3475edba5..e6bd28464e065f4b185c30391c982cabfa16e0f1 100644 (file)
@@ -152,6 +152,7 @@ dnsdist_SOURCES = \
        dnsdist-lua-vars.cc \
        dnsdist-prometheus.hh \
        dnsdist-protobuf.cc dnsdist-protobuf.hh \
+       dnsdist-proxy-protocol.cc dnsdist-proxy-protocol.hh \
        dnsdist-rings.cc dnsdist-rings.hh \
        dnsdist-rules.hh \
        dnsdist-secpoll.cc dnsdist-secpoll.hh \
@@ -180,6 +181,7 @@ dnsdist_SOURCES = \
        namespaces.hh \
        pdnsexception.hh \
        protobuf.cc protobuf.hh \
+       proxy-protocol.cc proxy-protocol.hh \
        dnstap.cc dnstap.hh \
        qtype.cc qtype.hh \
        remote_logger.cc remote_logger.hh \
@@ -215,6 +217,7 @@ testrunner_SOURCES = \
        test-dnsparser_cc.cc \
        test-iputils_hh.cc \
        test-mplexer.cc \
+       test-proxy_protocol_cc.cc \
        cachecleaner.hh \
        circular_buffer.hh \
        dnsdist.hh \
@@ -243,6 +246,7 @@ testrunner_SOURCES = \
        namespaces.hh \
        pdnsexception.hh \
        pollmplexer.cc \
+       proxy-protocol.cc proxy-protocol.hh \
        qtype.cc qtype.hh \
        sholder.hh \
        sodcrypto.cc \
index 3dc7e6a371dc8661fbbab7c6d5ed68123c9fde16..05ea1517173bc1ab6101396d8bb89a1bcc1380e3 100644 (file)
@@ -207,6 +207,11 @@ bool queueHealthCheck(std::shared_ptr<FDMultiplexer>& mplexer, const std::shared
     dnsheader * requestHeader = dpw.getHeader();
     *requestHeader = checkHeader;
 
+    if (ds->useProxyProtocol) {
+      auto payload = makeLocalProxyHeader();
+      packet.insert(packet.begin(), payload.begin(), payload.end());
+    }
+
     Socket sock(ds->remote.sin4.sin_family, SOCK_DGRAM);
     sock.setNonBlocking();
     if (!IsAnyAddress(ds->sourceAddr)) {
index 169ba64f3a12319e36efd1efaac7fd4f4899f24d..05fa48f7ff37ceaa307e7c2de959ecf4a5b66692 100644 (file)
@@ -3,7 +3,6 @@
 
 DNSResponse makeDNSResponseFromIDState(IDState& ids, struct dnsheader* dh, size_t bufferSize, uint16_t responseLen, bool isTCP)
 {
-  
   DNSResponse dr(&ids.qname, ids.qtype, ids.qclass, ids.qname.wirelength(), &ids.origDest, &ids.origRemote, dh, bufferSize, responseLen, isTCP, &ids.sentTime.d_start);
   dr.origFlags = ids.origFlags;
   dr.ecsAdded = ids.ecsAdded;
@@ -25,7 +24,7 @@ DNSResponse makeDNSResponseFromIDState(IDState& ids, struct dnsheader* dh, size_
     dr.dnsCryptQuery = std::move(ids.dnsCryptQuery);
   }
 
-  return dr;  
+  return dr;
 }
 
 void setIDStateFromDNSQuestion(IDState& ids, DNSQuestion& dq, DNSName&& qname)
@@ -49,9 +48,9 @@ void setIDStateFromDNSQuestion(IDState& ids, DNSQuestion& dq, DNSName&& qname)
   ids.useZeroScope = dq.useZeroScope;
   ids.qTag = dq.qTag;
   ids.dnssecOK = dq.dnssecOK;
-  
+
   ids.dnsCryptQuery = std::move(dq.dnsCryptQuery);
-  
+
 #ifdef HAVE_PROTOBUF
   ids.uniqueId = std::move(dq.uniqueId);
 #endif
diff --git a/pdns/dnsdistdist/dnsdist-proxy-protocol.cc b/pdns/dnsdistdist/dnsdist-proxy-protocol.cc
new file mode 100644 (file)
index 0000000..e689902
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * 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 "dnsdist-proxy-protocol.hh"
+
+bool addProxyProtocol(DNSQuestion& dq)
+{
+  auto payload = makeProxyHeader(dq.tcp, *dq.remote, *dq.local, dq.proxyProtocolValues ? *dq.proxyProtocolValues : std::vector<ProxyProtocolValue>());
+  if ((dq.size - dq.len) < payload.size()) {
+    return false;
+  }
+
+  memmove(reinterpret_cast<char*>(dq.dh) + payload.size(), dq.dh, dq.len);
+  memcpy(dq.dh, payload.c_str(), payload.size());
+  dq.len += payload.size();
+
+  return true;
+}
+
+bool addProxyProtocol(std::vector<uint8_t>& buffer, bool tcp, const ComboAddress& source, const ComboAddress& destination, const std::vector<ProxyProtocolValue>& values)
+{
+  auto payload = makeProxyHeader(tcp, source, destination, values);
+
+  auto previousSize = buffer.size();
+  buffer.resize(previousSize + payload.size());
+  std::copy_backward(buffer.begin(), buffer.begin() + previousSize, buffer.end());
+  std::copy(payload.begin(), payload.end(), buffer.begin());
+
+  return true;
+}
diff --git a/pdns/dnsdistdist/dnsdist-proxy-protocol.hh b/pdns/dnsdistdist/dnsdist-proxy-protocol.hh
new file mode 100644 (file)
index 0000000..433a7d2
--- /dev/null
@@ -0,0 +1,27 @@
+/*
+ * 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 "dnsdist.hh"
+
+bool addProxyProtocol(DNSQuestion& dq);
+bool addProxyProtocol(std::vector<uint8_t>& buffer, bool tcp, const ComboAddress& source, const ComboAddress& destination, const std::vector<ProxyProtocolValue>& values);
index cb51d14bc6557c9f5ce8e169780398b23857d31b..48ff00bcb8f9a2d454462565bb14ba0a97e13e8b 100644 (file)
@@ -407,6 +407,9 @@ Servers
   .. versionchanged:: 1.4.0
     Added ``checkInterval``, ``checkTimeout`` and ``rise`` to server_table.
 
+  .. versionchanged:: 1.5.0
+    Added ``useProxyProtocol`` to server_table.
+
   Add a new backend server. Call this function with either a string::
 
     newServer(
@@ -449,7 +452,8 @@ Servers
                              -- using the experimental XPF record from `draft-bellis-dnsop-xpf <https://datatracker.ietf.org/doc/draft-bellis-dnsop-xpf/>`_ and the specified option code. Default is disabled (0)
       sockets=NUM,           -- Number of sockets (and thus source ports) used toward the backend server, defaults to a single one
       disableZeroScope=BOOL, -- Disable the EDNS Client Subnet 'zero scope' feature, which does a cache lookup for an answer valid for all subnets (ECS scope of 0) before adding ECS information to the query and doing the regular lookup. This requires the ``parseECS`` option of the corresponding cache to be set to true
-      rise=NUM               -- Require NUM consecutive successful checks before declaring the backend up, default: 1
+      rise=NUM,              -- Require NUM consecutive successful checks before declaring the backend up, default: 1
+      useProxyProtocol=BOOL  -- Add a proxy protocol header to the query, passing along the client's IP address and port along with the original destination address and port. Default is disabled.
     })
 
   :param str server_string: A simple IP:PORT string.
index da2672f7534ca04c8a94ba5054a1b96f54e33830..b1b4163c9a9cc84a0b36dd5b33167707fde25e47 100644 (file)
@@ -204,6 +204,14 @@ This state can be modified from the various hooks.
     :param int expire: The value of the expire field in the SOA record
     :param int minimum: The value of the minimum field in the SOA record
 
+  .. method:: DNSQuestion:setProxyProtocolValues(values)
+
+    .. versionadded:: 1.5.0
+
+    Set the Proxy-Protocol Type-Length values to send to the backend along with this query.
+
+    :param table values: A table of types and values to send, for example: ``{ [0] = foo", [42] = "bar" }``
+
   .. method:: DNSQuestion:setTag(key, value)
 
     .. versionadded:: 1.2.0
index c534787af791e4dccbd8e9fb42cf987d46546151..f44ec557666cd3a0509906cf059812b0cbcdb3a7 100644 (file)
@@ -1251,6 +1251,14 @@ The following actions exist.
   * ``ad``: bool - Set the AD bit to this value (true means the bit is set, false means it's cleared). Default is to clear it.
   * ``ra``: bool - Set the RA bit to this value (true means the bit is set, false means it's cleared). Default is to copy the value of the RD bit from the incoming query.
 
+.. function:: SetProxyProtocolValuesAction(values)
+
+  .. versionadded:: 1.5.0
+
+  Set the Proxy-Protocol Type-Length values to be sent to the server along with this query to ``values``.
+
+  :param table values: A table of types and values to send, for example: ``{ [0] = foo", [42] = "bar" }``
+
 .. function:: SkipCacheAction()
 
   Don't lookup the cache for this query, don't store the answer.
index fcddb9d92964c3dfabf95b9502c06099041de9ae..6abf4b304e25e51326bb78fdc48e2d55b790fbab 100644 (file)
@@ -24,6 +24,7 @@
 #include "dns.hh"
 #include "dolog.hh"
 #include "dnsdist-ecs.hh"
+#include "dnsdist-proxy-protocol.hh"
 #include "dnsdist-rules.hh"
 #include "dnsdist-xpf.hh"
 #include "libssl.hh"
@@ -502,6 +503,10 @@ static int processDOHQuery(DOHUnit* du)
 
     dh->id = idOffset;
 
+    if (ss->useProxyProtocol) {
+      addProxyProtocol(dq);
+    }
+
     int fd = pickBackendSocketForSending(ss);
     try {
       /* you can't touch du after this line, because it might already have been freed */
diff --git a/pdns/dnsdistdist/proxy-protocol.cc b/pdns/dnsdistdist/proxy-protocol.cc
new file mode 120000 (symlink)
index 0000000..ae6a943
--- /dev/null
@@ -0,0 +1 @@
+../proxy-protocol.cc
\ No newline at end of file
diff --git a/pdns/dnsdistdist/proxy-protocol.hh b/pdns/dnsdistdist/proxy-protocol.hh
new file mode 120000 (symlink)
index 0000000..bc45ee8
--- /dev/null
@@ -0,0 +1 @@
+../proxy-protocol.hh
\ No newline at end of file
diff --git a/pdns/dnsdistdist/test-proxy_protocol_cc.cc b/pdns/dnsdistdist/test-proxy_protocol_cc.cc
new file mode 120000 (symlink)
index 0000000..6350ca4
--- /dev/null
@@ -0,0 +1 @@
+../test-proxy_protocol_cc.cc
\ No newline at end of file
diff --git a/pdns/fuzz_proxyprotocol.cc b/pdns/fuzz_proxyprotocol.cc
new file mode 100644 (file)
index 0000000..d138d6f
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * 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 "proxy-protocol.hh"
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+
+  std::vector<ProxyProtocolValue> values;
+  ComboAddress source;
+  ComboAddress destination;
+  bool proxy = false;
+  bool tcp = false;  
+
+  try {
+    parseProxyHeader(std::string(reinterpret_cast<const char*>(data), size), proxy, source, destination, tcp, values);
+  }
+  catch(const std::exception& e) {
+  }
+  catch(const PDNSException& e) {
+  }
+
+  return 0;
+}
index 47396a8386ecaa3607122712f6b7cba4de403e81..d852fbeb05703287e480c6c0e80da6c17f8f7e24 100644 (file)
@@ -29,6 +29,12 @@ extern "C" {
     const void* data;
   } pdns_ednsoption_t;
 
+  typedef struct pdns_proxyprotocol_value {
+    uint8_t     type;
+    uint16_t    len;
+    const void* data;
+  } pdns_proxyprotocol_value_t;
+
   typedef enum
   {
     answer = 1,
@@ -53,6 +59,9 @@ extern "C" {
   size_t pdns_ffi_param_get_edns_options(pdns_ffi_param_t* ref, const pdns_ednsoption_t** out) __attribute__ ((visibility ("default")));
   size_t pdns_ffi_param_get_edns_options_by_code(pdns_ffi_param_t* ref, uint16_t optionCode, const pdns_ednsoption_t** out) __attribute__ ((visibility ("default")));
 
+  // returns the length of the resulting 'out' array. 'out' is not set if the length is 0
+  size_t pdns_ffi_param_get_proxy_protocol_values(pdns_ffi_param_t* ref, const pdns_proxyprotocol_value_t** out) __attribute__ ((visibility ("default")));
+
   void pdns_ffi_param_set_tag(pdns_ffi_param_t* ref, unsigned int tag) __attribute__ ((visibility ("default")));
   void pdns_ffi_param_add_policytag(pdns_ffi_param_t *ref, const char* name) __attribute__ ((visibility ("default")));
   void pdns_ffi_param_set_requestorid(pdns_ffi_param_t* ref, const char* name) __attribute__ ((visibility ("default")));
index 36239b0e14a779b788e7bbd0b6c19cab3fcef7be..404a773d8dd91b530283b96b713e9df41c15052b 100644 (file)
@@ -183,6 +183,20 @@ boost::optional<Netmask>  RecursorLua4::DNSQuestion::getEDNSSubnet() const
   return boost::optional<Netmask>();
 }
 
+std::vector<std::pair<int, ProxyProtocolValue>> RecursorLua4::DNSQuestion::getProxyProtocolValues() const
+{
+  std::vector<std::pair<int, ProxyProtocolValue>> result;
+  if (proxyProtocolValues) {
+    result.reserve(proxyProtocolValues->size());
+
+    int idx = 1;
+    for (const auto& value: *proxyProtocolValues) {
+      result.push_back({ idx++, value });
+    }
+  }
+
+  return result;
+}
 
 vector<pair<int, DNSRecord> > RecursorLua4::DNSQuestion::getRecords() const
 {
@@ -291,6 +305,7 @@ void RecursorLua4::postPrepareContext()
   d_lw->registerFunction("getEDNSOptions", &DNSQuestion::getEDNSOptions);
   d_lw->registerFunction("getEDNSOption", &DNSQuestion::getEDNSOption);
   d_lw->registerFunction("getEDNSSubnet", &DNSQuestion::getEDNSSubnet);
+  d_lw->registerFunction("getProxyProtocolValues", &DNSQuestion::getProxyProtocolValues);
   d_lw->registerFunction("getEDNSFlags", &DNSQuestion::getEDNSFlags);
   d_lw->registerFunction("getEDNSFlag", &DNSQuestion::getEDNSFlag);
   d_lw->registerMember("name", &DNSRecord::d_name);
@@ -336,6 +351,8 @@ void RecursorLua4::postPrepareContext()
       return ret;
     });
 
+  d_lw->registerFunction<const ProxyProtocolValue, std::string()>("getContent", [](const ProxyProtocolValue& value) { return value.content; });
+  d_lw->registerFunction<const ProxyProtocolValue, uint8_t()>("getType", [](const ProxyProtocolValue& value) { return value.type; });
 
   d_lw->registerFunction<void(DNSRecord::*)(const std::string&)>("changeContent", [](DNSRecord& dr, const std::string& newContent) { dr.d_content = DNSRecordContent::mastermake(dr.d_type, 1, newContent); });
   d_lw->registerFunction("addAnswer", &DNSQuestion::addAnswer);
@@ -520,10 +537,17 @@ bool RecursorLua4::ipfilter(const ComboAddress& remote, const ComboAddress& loca
   return false; // don't block
 }
 
-unsigned int RecursorLua4::gettag(const ComboAddress& remote, const Netmask& ednssubnet, const ComboAddress& local, const DNSName& qname, uint16_t qtype, std::vector<std::string>* policyTags, LuaContext::LuaObject& data, const EDNSOptionViewMap& ednsOptions, bool tcp, std::string& requestorId, std::string& deviceId, std::string& deviceName) const
+unsigned int RecursorLua4::gettag(const ComboAddress& remote, const Netmask& ednssubnet, const ComboAddress& local, const DNSName& qname, uint16_t qtype, std::vector<std::string>* policyTags, LuaContext::LuaObject& data, const EDNSOptionViewMap& ednsOptions, bool tcp, std::string& requestorId, std::string& deviceId, std::string& deviceName, const std::vector<ProxyProtocolValue>& proxyProtocolValues) const
 {
   if(d_gettag) {
-    auto ret = d_gettag(remote, ednssubnet, local, qname, qtype, ednsOptions, tcp);
+    std::vector<std::pair<int, const ProxyProtocolValue*>> proxyProtocolValuesMap;
+    proxyProtocolValuesMap.reserve(proxyProtocolValues.size());
+    int num = 1;
+    for (const auto& value : proxyProtocolValues) {
+      proxyProtocolValuesMap.emplace_back(num++, &value);
+    }
+
+    auto ret = d_gettag(remote, ednssubnet, local, qname, qtype, ednsOptions, tcp, proxyProtocolValuesMap);
 
     if (policyTags) {
       const auto& tags = std::get<1>(ret);
@@ -558,7 +582,7 @@ unsigned int RecursorLua4::gettag(const ComboAddress& remote, const Netmask& edn
 struct pdns_ffi_param
 {
 public:
-  pdns_ffi_param(const DNSName& qname_, uint16_t qtype_, const ComboAddress& local_, const ComboAddress& remote_, const Netmask& ednssubnet_, std::vector<std::string>& policyTags_, std::vector<DNSRecord>& records_, const EDNSOptionViewMap& ednsOptions_, std::string& requestorId_, std::string& deviceId_, std::string& deviceName_, boost::optional<int>& rcode_, uint32_t& ttlCap_, bool& variable_, bool tcp_, bool& logQuery_, bool& logResponse_, bool& followCNAMERecords_): qname(qname_), local(local_), remote(remote_), ednssubnet(ednssubnet_), policyTags(policyTags_), records(records_), ednsOptions(ednsOptions_), requestorId(requestorId_), deviceId(deviceId_), deviceName(deviceName_), rcode(rcode_), ttlCap(ttlCap_), variable(variable_), logQuery(logQuery_), logResponse(logResponse_), followCNAMERecords(followCNAMERecords_), qtype(qtype_), tcp(tcp_)
+  pdns_ffi_param(const DNSName& qname_, uint16_t qtype_, const ComboAddress& local_, const ComboAddress& remote_, const Netmask& ednssubnet_, std::vector<std::string>& policyTags_, std::vector<DNSRecord>& records_, const EDNSOptionViewMap& ednsOptions_, const std::vector<ProxyProtocolValue>& proxyProtocolValues_, std::string& requestorId_, std::string& deviceId_, std::string& deviceName_, boost::optional<int>& rcode_, uint32_t& ttlCap_, bool& variable_, bool tcp_, bool& logQuery_, bool& logResponse_, bool& followCNAMERecords_): qname(qname_), local(local_), remote(remote_), ednssubnet(ednssubnet_), policyTags(policyTags_), records(records_), ednsOptions(ednsOptions_), proxyProtocolValues(proxyProtocolValues_), requestorId(requestorId_), deviceId(deviceId_), deviceName(deviceName_), rcode(rcode_), ttlCap(ttlCap_), variable(variable_), logQuery(logQuery_), logResponse(logResponse_), followCNAMERecords(followCNAMERecords_), qtype(qtype_), tcp(tcp_)
   {
   }
 
@@ -567,6 +591,7 @@ public:
   std::unique_ptr<std::string> remoteStr{nullptr};
   std::unique_ptr<std::string> ednssubnetStr{nullptr};
   std::vector<pdns_ednsoption_t> ednsOptionsVect;
+  std::vector<pdns_proxyprotocol_value_t> proxyProtocolValuesVect;
 
   const DNSName& qname;
   const ComboAddress& local;
@@ -575,6 +600,7 @@ public:
   std::vector<std::string>& policyTags;
   std::vector<DNSRecord>& records;
   const EDNSOptionViewMap& ednsOptions;
+  const std::vector<ProxyProtocolValue>& proxyProtocolValues;
   std::string& requestorId;
   std::string& deviceId;
   std::string& deviceName;
@@ -590,10 +616,10 @@ public:
   bool tcp;
 };
 
-unsigned int RecursorLua4::gettag_ffi(const ComboAddress& remote, const Netmask& ednssubnet, const ComboAddress& local, const DNSName& qname, uint16_t qtype, std::vector<std::string>* policyTags, std::vector<DNSRecord>& records, LuaContext::LuaObject& data, const EDNSOptionViewMap& ednsOptions, bool tcp, std::string& requestorId, std::string& deviceId, std::string& deviceName, boost::optional<int>& rcode, uint32_t& ttlCap, bool& variable, bool& logQuery, bool& logResponse, bool& followCNAMERecords) const
+unsigned int RecursorLua4::gettag_ffi(const ComboAddress& remote, const Netmask& ednssubnet, const ComboAddress& local, const DNSName& qname, uint16_t qtype, std::vector<std::string>* policyTags, std::vector<DNSRecord>& records, LuaContext::LuaObject& data, const EDNSOptionViewMap& ednsOptions, bool tcp, const std::vector<ProxyProtocolValue>& proxyProtocolValues, std::string& requestorId, std::string& deviceId, std::string& deviceName, boost::optional<int>& rcode, uint32_t& ttlCap, bool& variable, bool& logQuery, bool& logResponse, bool& followCNAMERecords) const
 {
   if (d_gettag_ffi) {
-    pdns_ffi_param_t param(qname, qtype, local, remote, ednssubnet, *policyTags, records, ednsOptions, requestorId, deviceId, deviceName, rcode, ttlCap, variable, tcp, logQuery, logResponse, followCNAMERecords);
+    pdns_ffi_param_t param(qname, qtype, local, remote, ednssubnet, *policyTags, records, ednsOptions, proxyProtocolValues, requestorId, deviceId, deviceName, rcode, ttlCap, variable, tcp, logQuery, logResponse, followCNAMERecords);
 
     auto ret = d_gettag_ffi(&param);
     if (ret) {
@@ -823,6 +849,30 @@ size_t pdns_ffi_param_get_edns_options_by_code(pdns_ffi_param_t* ref, uint16_t o
   return pos;
 }
 
+size_t pdns_ffi_param_get_proxy_protocol_values(pdns_ffi_param_t* ref, const pdns_proxyprotocol_value_t** out)
+{
+  if (ref->proxyProtocolValues.empty()) {
+    return 0;
+  }
+
+  ref->proxyProtocolValuesVect.resize(ref->proxyProtocolValues.size());
+
+  size_t pos = 0;
+  for (const auto& value : ref->proxyProtocolValues) {
+    auto& dest = ref->proxyProtocolValuesVect.at(pos);
+    dest.type = value.type;
+    dest.len = value.content.size();
+    if (dest.len > 0) {
+      dest.data = value.content.data();
+    }
+    pos++;
+  }
+
+  *out = ref->proxyProtocolValuesVect.data();
+
+  return ref->proxyProtocolValuesVect.size();
+}
+
 void pdns_ffi_param_set_tag(pdns_ffi_param_t* ref, unsigned int tag)
 {
   ref->tag = tag;
index 4515136a4a650aaec0abecd1de75484d384a1671..88bf6fea8ccd908f0fa54b04d4b46a1698ea0770 100644 (file)
@@ -33,6 +33,8 @@
 #include "ednsoptions.hh"
 #include "validate.hh"
 #include "lua-base4.hh"
+#include "proxy-protocol.hh"
+
 #include <unordered_map>
 
 #include "lua-recursor4-ffi.hh"
@@ -74,6 +76,7 @@ public:
     vector<DNSRecord>* currentRecords{nullptr};
     DNSFilterEngine::Policy* appliedPolicy{nullptr};
     std::vector<std::string>* policyTags{nullptr};
+    const std::vector<ProxyProtocolValue>* proxyProtocolValues{nullptr};
     std::unordered_map<std::string,bool>* discardedPolicies{nullptr};
     std::string requestorId;
     std::string deviceId;
@@ -91,6 +94,7 @@ public:
     vector<pair<uint16_t, string> > getEDNSOptions() const;
     boost::optional<string> getEDNSOption(uint16_t code) const;
     boost::optional<Netmask> getEDNSSubnet() const;
+    std::vector<std::pair<int, ProxyProtocolValue>> getProxyProtocolValues() const;
     vector<string> getEDNSFlags() const;
     bool getEDNSFlag(string flag) const;
     void setRecords(const vector<pair<int,DNSRecord> >& records);
@@ -111,8 +115,8 @@ public:
     DNSName followupName;
   };
 
-  unsigned int gettag(const ComboAddress& remote, const Netmask& ednssubnet, const ComboAddress& local, const DNSName& qname, uint16_t qtype, std::vector<std::string>* policyTags, LuaContext::LuaObject& data, const EDNSOptionViewMap&, bool tcp, std::string& requestorId, std::string& deviceId, std::string& deviceName) const;
-  unsigned int gettag_ffi(const ComboAddress& remote, const Netmask& ednssubnet, const ComboAddress& local, const DNSName& qname, uint16_t qtype, std::vector<std::string>* policyTags, std::vector<DNSRecord>& records, LuaContext::LuaObject& data, const EDNSOptionViewMap& ednsOptions, bool tcp, std::string& requestorId, std::string& deviceId, std::string& deviceName, boost::optional<int>& rcode, uint32_t& ttlCap, bool& variable, bool& logQuery, bool& logResponse, bool& followCNAMERecords) const;
+  unsigned int gettag(const ComboAddress& remote, const Netmask& ednssubnet, const ComboAddress& local, const DNSName& qname, uint16_t qtype, std::vector<std::string>* policyTags, LuaContext::LuaObject& data, const EDNSOptionViewMap&, bool tcp, std::string& requestorId, std::string& deviceId, std::string& deviceName, const std::vector<ProxyProtocolValue>& proxyProtocolValues) const;
+  unsigned int gettag_ffi(const ComboAddress& remote, const Netmask& ednssubnet, const ComboAddress& local, const DNSName& qname, uint16_t qtype, std::vector<std::string>* policyTags, std::vector<DNSRecord>& records, LuaContext::LuaObject& data, const EDNSOptionViewMap& ednsOptions, bool tcp, const std::vector<ProxyProtocolValue>& proxyProtocolValues, std::string& requestorId, std::string& deviceId, std::string& deviceName, boost::optional<int>& rcode, uint32_t& ttlCap, bool& variable, bool& logQuery, bool& logResponse, bool& followCNAMERecords) const;
 
   void maintenance() const;
   bool prerpz(DNSQuestion& dq, int& ret) const;
@@ -133,7 +137,7 @@ public:
             d_postresolve);
   }
 
-  typedef std::function<std::tuple<unsigned int,boost::optional<std::unordered_map<int,string> >,boost::optional<LuaContext::LuaObject>,boost::optional<std::string>,boost::optional<std::string>,boost::optional<std::string> >(ComboAddress, Netmask, ComboAddress, DNSName, uint16_t, const EDNSOptionViewMap&, bool)> gettag_t;
+  typedef std::function<std::tuple<unsigned int,boost::optional<std::unordered_map<int,string> >,boost::optional<LuaContext::LuaObject>,boost::optional<std::string>,boost::optional<std::string>,boost::optional<std::string> >(ComboAddress, Netmask, ComboAddress, DNSName, uint16_t, const EDNSOptionViewMap&, bool, const std::vector<std::pair<int, const ProxyProtocolValue*>>&)> gettag_t;
   gettag_t d_gettag; // public so you can query if we have this hooked
   typedef std::function<boost::optional<LuaContext::LuaObject>(pdns_ffi_param_t*)> gettag_ffi_t;
   gettag_ffi_t d_gettag_ffi;
index 8ad11b56ac62011df7edd1ae2cc2fff0bec15cae..088cad58e3bdd023a8a7d265e7a34985b53bedb7 100644 (file)
@@ -88,6 +88,7 @@
 #include "rec-lua-conf.hh"
 #include "ednsoptions.hh"
 #include "gettime.hh"
+#include "proxy-protocol.hh"
 #include "pubsuffix.hh"
 #ifdef NOD_ENABLED
 #include "nod.hh"
@@ -197,6 +198,8 @@ 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 NetmaskGroup g_proxyProtocolACL;
+static size_t g_proxyProtocolMaximumSize;
 static size_t g_tcpMaxQueriesPerConn;
 static size_t s_maxUDPQueriesPerRound;
 static uint64_t g_latencyStatSize;
@@ -305,6 +308,7 @@ struct DNSComboWriter {
     return d_source.toStringWithPort() + " (proxied by " + d_remote.toStringWithPort() + ")";
   }
 
+  std::vector<ProxyProtocolValue> d_proxyProtocolValues;
   MOADNSParser d_mdp;
   struct timeval d_now;
   /* Remote client, might differ from d_source
@@ -1283,6 +1287,7 @@ static void startDoResolve(void *p)
     dq.deviceId = dc->d_deviceId;
     dq.deviceName = dc->d_deviceName;
 #endif
+    dq.proxyProtocolValues = &dc->d_proxyProtocolValues;
 
     if(ednsExtRCode != 0) {
       goto sendit;
@@ -1995,11 +2000,93 @@ static void getQNameAndSubnet(const std::string& question, DNSName* dnsname, uin
   }
 }
 
+static bool handleTCPReadResult(int fd, ssize_t bytes)
+{
+  if (bytes == 0) {
+    /* EOF */
+    t_fdm->removeReadFD(fd);
+    return false;
+  }
+  else if (bytes < 0) {
+    if (errno != EAGAIN && errno != EWOULDBLOCK) {
+      t_fdm->removeReadFD(fd);
+      return false;
+    }
+  }
+
+  return true;
+}
+
 static void handleRunningTCPQuestion(int fd, FDMultiplexer::funcparam_t& var)
 {
   shared_ptr<TCPConnection> conn=any_cast<shared_ptr<TCPConnection> >(var);
 
-  if(conn->state==TCPConnection::BYTE0) {
+  if (conn->state == TCPConnection::PROXYPROTOCOLHEADER) {
+    ssize_t bytes = recv(conn->getFD(), &conn->data.at(conn->proxyProtocolGot), conn->proxyProtocolNeed, 0);
+    if (bytes <= 0) {
+      handleTCPReadResult(fd, bytes);
+      return;
+    }
+
+    conn->proxyProtocolGot += bytes;
+    conn->data.resize(conn->proxyProtocolGot);
+    ssize_t remaining = isProxyHeaderComplete(conn->data);
+    if (remaining == 0) {
+      if (g_logCommonErrors) {
+        g_log<<Logger::Error<<"Unable to consume proxy protocol header in packet from TCP client "<< conn->d_remote.toStringWithPort() <<endl;
+      }
+      ++g_stats.proxyProtocolInvalidCount;
+      t_fdm->removeReadFD(fd);
+      return;
+    }
+    else if (remaining < 0) {
+      conn->proxyProtocolNeed = -remaining;
+      conn->data.resize(conn->proxyProtocolGot + conn->proxyProtocolNeed);
+      return;
+    }
+    else {
+      /* proxy header received */
+      /* we ignore the TCP field for now, but we could properly set whether
+         the connection was received over UDP or TCP if needed */
+      bool tcp;
+      bool proxy = false;
+      size_t used = parseProxyHeader(conn->data, proxy, conn->d_source, conn->d_destination, tcp, conn->proxyProtocolValues);
+      if (used <= 0) {
+        if (g_logCommonErrors) {
+          g_log<<Logger::Error<<"Unable to parse proxy protocol header in packet from TCP client "<< conn->d_remote.toStringWithPort() <<endl;
+        }
+        ++g_stats.proxyProtocolInvalidCount;
+        t_fdm->removeReadFD(fd);
+        return;
+      }
+      else if (static_cast<size_t>(used) > g_proxyProtocolMaximumSize) {
+        if (g_logCommonErrors) {
+          g_log<<Logger::Error<<"Proxy protocol header in packet from TCP client "<< conn->d_remote.toStringWithPort() << " is larger than proxy-protocol-maximum-size (" << used << "), dropping"<< endl;
+        }
+        ++g_stats.proxyProtocolInvalidCount;
+        t_fdm->removeReadFD(fd);
+        return;
+      }
+
+      /* Now that we have retrieved the address of the client, as advertised by the proxy
+         via the proxy protocol header, check that it is allowed by our ACL */
+      /* note that if the proxy header used a 'LOCAL' command, the original source and destination are untouched so everything should be fine */
+      if (t_allowFrom && !t_allowFrom->match(&conn->d_source)) {
+        if (!g_quiet) {
+          g_log<<Logger::Error<<"["<<MT->getTid()<<"] dropping TCP query from "<<conn->d_source.toString()<<", address not matched by allow-from"<<endl;
+        }
+
+        ++g_stats.unauthorizedTCP;
+        t_fdm->removeReadFD(fd);
+        return;
+      }
+
+      conn->data.resize(2);
+      conn->state = TCPConnection::BYTE0;
+    }
+  }
+
+  if (conn->state==TCPConnection::BYTE0) {
     ssize_t bytes=recv(conn->getFD(), &conn->data[0], 2, 0);
     if(bytes==1)
       conn->state=TCPConnection::BYTE1;
@@ -2009,12 +2096,13 @@ static void handleRunningTCPQuestion(int fd, FDMultiplexer::funcparam_t& var)
       conn->bytesread=0;
       conn->state=TCPConnection::GETQUESTION;
     }
-    if(!bytes || bytes < 0) {
-      t_fdm->removeReadFD(fd);
+    if (bytes <= 0) {
+      handleTCPReadResult(fd, bytes);
       return;
     }
   }
-  else if(conn->state==TCPConnection::BYTE1) {
+
+  if (conn->state==TCPConnection::BYTE1) {
     ssize_t bytes=recv(conn->getFD(), &conn->data[1], 1, 0);
     if(bytes==1) {
       conn->state=TCPConnection::GETQUESTION;
@@ -2022,18 +2110,29 @@ static void handleRunningTCPQuestion(int fd, FDMultiplexer::funcparam_t& var)
       conn->data.resize(conn->qlen);
       conn->bytesread=0;
     }
-    if(!bytes || bytes < 0) {
-      if(g_logCommonErrors)
-        g_log<<Logger::Error<<"TCP client "<< conn->d_remote.toStringWithPort() <<" disconnected after first byte"<<endl;
-      t_fdm->removeReadFD(fd);
+    if (bytes <= 0) {
+      if (!handleTCPReadResult(fd, bytes)) {
+        if(g_logCommonErrors) {
+          g_log<<Logger::Error<<"TCP client "<< conn->d_remote.toStringWithPort() <<" disconnected after first byte"<<endl;
+        }
+      }
       return;
     }
   }
-  else if(conn->state==TCPConnection::GETQUESTION) {
+
+  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()) {
+    if (bytes <= 0) {
+      if (!handleTCPReadResult(fd, bytes)) {
+        if(g_logCommonErrors) {
+          g_log<<Logger::Error<<"TCP client "<< conn->d_remote.toStringWithPort() <<" disconnected while reading question body"<<endl;
+        }
+      }
+      return;
+    }
+    else if (bytes > std::numeric_limits<std::uint16_t>::max()) {
       if(g_logCommonErrors) {
-        g_log<<Logger::Error<<"TCP client "<< conn->d_remote.toStringWithPort() <<" disconnected while reading question body"<<endl;
+        g_log<<Logger::Error<<"TCP client "<< conn->d_remote.toStringWithPort() <<" sent an invalid question size while reading question body"<<endl;
       }
       t_fdm->removeReadFD(fd);
       return;
@@ -2055,14 +2154,17 @@ static void handleRunningTCPQuestion(int fd, FDMultiplexer::funcparam_t& var)
       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->setSource(conn->d_remote);
+      dc->setSource(conn->d_source);
       ComboAddress dest;
       dest.reset();
       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);
+      dc->setDestination(conn->d_destination);
+      /* we can't move this if we want to be able to access the values in
+         all queries sent over this connection */
+      dc->d_proxyProtocolValues = conn->proxyProtocolValues;
       DNSName qname;
       uint16_t qtype=0;
       uint16_t qclass=0;
@@ -2099,10 +2201,10 @@ static void handleRunningTCPQuestion(int fd, FDMultiplexer::funcparam_t& var)
           if(t_pdl) {
             try {
               if (t_pdl->d_gettag_ffi) {
-                dc->d_tag = t_pdl->gettag_ffi(dc->d_source, dc->d_ednssubnet.source, dc->d_destination, qname, qtype, &dc->d_policyTags, dc->d_records, dc->d_data, ednsOptions, true, requestorId, deviceId, deviceName, dc->d_rcode, dc->d_ttlCap, dc->d_variable, logQuery, dc->d_logResponse, dc->d_followCNAMERecords);
+                dc->d_tag = t_pdl->gettag_ffi(dc->d_source, dc->d_ednssubnet.source, dc->d_destination, qname, qtype, &dc->d_policyTags, dc->d_records, dc->d_data, ednsOptions, true, dc->d_proxyProtocolValues, requestorId, deviceId, deviceName, dc->d_rcode, dc->d_ttlCap, dc->d_variable, logQuery, dc->d_logResponse, dc->d_followCNAMERecords);
               }
               else if (t_pdl->d_gettag) {
-                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, deviceName);
+                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, deviceName, dc->d_proxyProtocolValues);
               }
             }
             catch(const std::exception& e)  {
@@ -2189,6 +2291,11 @@ static void handleRunningTCPQuestion(int fd, FDMultiplexer::funcparam_t& var)
   }
 }
 
+static bool expectProxyProtocol(const ComboAddress& from)
+{
+  return g_proxyProtocolACL.match(from);
+}
+
 //! Handle new incoming TCP connection
 static void handleNewTCPQuestion(int fd, FDMultiplexer::funcparam_t& )
 {
@@ -2207,11 +2314,14 @@ static void handleNewTCPQuestion(int fd, FDMultiplexer::funcparam_t& )
       return;
     }
 
-    if(t_remotes)
+    if(t_remotes) {
       t_remotes->push_back(addr);
-    if(t_allowFrom && !t_allowFrom->match(&addr)) {
+    }
+
+    bool fromProxyProtocolSource = expectProxyProtocol(addr);
+    if(t_allowFrom && !t_allowFrom->match(&addr) && !fromProxyProtocolSource) {
       if(!g_quiet)
-        g_log<<Logger::Error<<"["<<MT->getTid()<<"] dropping TCP query from "<<addr.toString()<<", address not matched by allow-from"<<endl;
+        g_log<<Logger::Error<<"["<<MT->getTid()<<"] dropping TCP query from "<<addr.toString()<<", address neither matched by allow-from nor proxy-protocol-from"<<endl;
 
       g_stats.unauthorizedTCP++;
       try {
@@ -2222,6 +2332,7 @@ static void handleNewTCPQuestion(int fd, FDMultiplexer::funcparam_t& )
       }
       return;
     }
+
     if(g_maxTCPPerClient && t_tcpClientCounts->count(addr) && (*t_tcpClientCounts)[addr] >= g_maxTCPPerClient) {
       g_stats.tcpClientOverflow++;
       try {
@@ -2235,7 +2346,20 @@ static void handleNewTCPQuestion(int fd, FDMultiplexer::funcparam_t& )
 
     setNonBlocking(newsock);
     std::shared_ptr<TCPConnection> tc = std::make_shared<TCPConnection>(newsock, addr);
-    tc->state=TCPConnection::BYTE0;
+    tc->d_source = addr;
+    tc->d_destination.reset();
+    tc->d_destination.sin4.sin_family = addr.sin4.sin_family;
+    socklen_t len = tc->d_destination.getSocklen();
+    getsockname(tc->getFD(), reinterpret_cast<sockaddr*>(&tc->d_destination), &len); // if this fails, we're ok with it
+
+    if (fromProxyProtocolSource) {
+      tc->proxyProtocolNeed = s_proxyProtocolMinimumHeaderSize;
+      tc->data.resize(tc->proxyProtocolNeed);
+      tc->state = TCPConnection::PROXYPROTOCOLHEADER;
+    }
+    else {
+      tc->state = TCPConnection::BYTE0;
+    }
 
     struct timeval ttd;
     Utility::gettimeofday(&ttd, 0);
@@ -2245,7 +2369,7 @@ static void handleNewTCPQuestion(int fd, FDMultiplexer::funcparam_t& )
   }
 }
 
-static string* doProcessUDPQuestion(const std::string& question, const ComboAddress& fromaddr, const ComboAddress& destaddr, struct timeval tv, int fd)
+static string* doProcessUDPQuestion(const std::string& question, const ComboAddress& fromaddr, const ComboAddress& destaddr, ComboAddress source, ComboAddress destination, struct timeval tv, int fd, std::vector<ProxyProtocolValue>& proxyProtocolValues)
 {
   gettimeofday(&g_now, 0);
   if (tv.tv_sec) {
@@ -2270,8 +2394,6 @@ static string* doProcessUDPQuestion(const std::string& question, const ComboAddr
   bool needXPF = g_XPFAcl.match(fromaddr);
   std::vector<std::string> policyTags;
   LuaContext::LuaObject data;
-  ComboAddress source = fromaddr;
-  ComboAddress destination = destaddr;
   string requestorId;
   string deviceId;
   string deviceName;
@@ -2337,10 +2459,10 @@ static string* doProcessUDPQuestion(const std::string& question, const ComboAddr
         if(t_pdl) {
           try {
             if (t_pdl->d_gettag_ffi) {
-              ctag = t_pdl->gettag_ffi(source, ednssubnet.source, destination, qname, qtype, &policyTags, records, data, ednsOptions, false, requestorId, deviceId, deviceName, rcode, ttlCap, variable, logQuery, logResponse, followCNAMEs);
+              ctag = t_pdl->gettag_ffi(source, ednssubnet.source, destination, qname, qtype, &policyTags, records, data, ednsOptions, false, proxyProtocolValues, requestorId, deviceId, deviceName, rcode, ttlCap, variable, logQuery, logResponse, followCNAMEs);
             }
             else if (t_pdl->d_gettag) {
-              ctag = t_pdl->gettag(source, ednssubnet.source, destination, qname, qtype, &policyTags, data, ednsOptions, false, requestorId, deviceId, deviceName);
+              ctag = t_pdl->gettag(source, ednssubnet.source, destination, qname, qtype, &policyTags, data, ednsOptions, false, requestorId, deviceId, deviceName, proxyProtocolValues);
             }
           }
           catch(const std::exception& e)  {
@@ -2488,6 +2610,7 @@ static string* doProcessUDPQuestion(const std::string& question, const ComboAddr
   dc->d_deviceName = deviceName;
   dc->d_kernelTimestamp = tv;
 #endif
+  dc->d_proxyProtocolValues = std::move(proxyProtocolValues);
 
   MT->makeThread(startDoResolve, (void*) dc.release()); // deletes dc
   return 0;
@@ -2497,15 +2620,19 @@ static string* doProcessUDPQuestion(const std::string& question, const ComboAddr
 static void handleNewUDPQuestion(int fd, FDMultiplexer::funcparam_t& var)
 {
   ssize_t len;
-  static const size_t maxIncomingQuerySize = 512;
+  static const size_t maxIncomingQuerySize = g_proxyProtocolACL.empty() ? 512 : (512 + g_proxyProtocolMaximumSize);
   static thread_local std::string data;
   ComboAddress fromaddr;
+  ComboAddress source;
+  ComboAddress destination;
   struct msghdr msgh;
   struct iovec iov;
   cmsgbuf_aligned cbuf;
   bool firstQuery = true;
+  std::vector<ProxyProtocolValue> proxyProtocolValues;
 
   for(size_t queriesCounter = 0; queriesCounter < s_maxUDPQueriesPerRound; queriesCounter++) {
+    bool proxyProto = false;
     data.resize(maxIncomingQuerySize);
     fromaddr.sin6.sin6_family=AF_INET6; // this makes sure fromaddr is big enough
     fillMSGHdr(&msgh, &iov, &cbuf, sizeof(cbuf), &data[0], data.size(), &fromaddr);
@@ -2514,34 +2641,70 @@ static void handleNewUDPQuestion(int fd, FDMultiplexer::funcparam_t& var)
 
       firstQuery = false;
 
-      if (static_cast<size_t>(len) < sizeof(dnsheader)) {
-        g_stats.ignoredCount++;
+      if (msgh.msg_flags & MSG_TRUNC) {
+        g_stats.truncatedDrops++;
         if (!g_quiet) {
-          g_log<<Logger::Error<<"Ignoring too-short ("<<std::to_string(len)<<") query from "<<fromaddr.toString()<<endl;
+          g_log<<Logger::Error<<"Ignoring truncated query from "<<fromaddr.toString()<<endl;
         }
         return;
       }
 
-      if (msgh.msg_flags & MSG_TRUNC) {
+      data.resize(static_cast<size_t>(len));
+
+      if (expectProxyProtocol(fromaddr)) {
+        bool tcp;
+        ssize_t used = parseProxyHeader(data, proxyProto, source, destination, tcp, proxyProtocolValues);
+        if (used <= 0) {
+          ++g_stats.proxyProtocolInvalidCount;
+          if (!g_quiet) {
+            g_log<<Logger::Error<<"Ignoring invalid proxy protocol ("<<std::to_string(len)<<", "<<std::to_string(used)<<") query from "<<fromaddr.toStringWithPort()<<endl;
+          }
+          return;
+        }
+        else if (static_cast<size_t>(used) > g_proxyProtocolMaximumSize) {
+          if (g_quiet) {
+            g_log<<Logger::Error<<"Proxy protocol header in UDP packet from "<< fromaddr.toStringWithPort() << " is larger than proxy-protocol-maximum-size (" << used << "), dropping"<< endl;
+          }
+          ++g_stats.proxyProtocolInvalidCount;
+          return;
+        }
+
+        data.erase(0, used);
+      }
+      else if (len > 512) {
+        /* we only allow UDP packets larger than 512 for those with a proxy protocol header */
         g_stats.truncatedDrops++;
         if (!g_quiet) {
-          g_log<<Logger::Error<<"Ignoring truncated query from "<<fromaddr.toString()<<endl;
+          g_log<<Logger::Error<<"Ignoring truncated query from "<<fromaddr.toStringWithPort()<<endl;
+        }
+        return;
+      }
+
+      if (data.size() < sizeof(dnsheader)) {
+        g_stats.ignoredCount++;
+        if (!g_quiet) {
+          g_log<<Logger::Error<<"Ignoring too-short ("<<std::to_string(data.size())<<") query from "<<fromaddr.toString()<<endl;
         }
         return;
       }
 
+      if (!proxyProto) {
+        source = fromaddr;
+      }
+
       if(t_remotes) {
         t_remotes->push_back(fromaddr);
       }
 
-      if(t_allowFrom && !t_allowFrom->match(&fromaddr)) {
+      if(t_allowFrom && !t_allowFrom->match(&source)) {
         if(!g_quiet) {
-          g_log<<Logger::Error<<"["<<MT->getTid()<<"] dropping UDP query from "<<fromaddr.toString()<<", address not matched by allow-from"<<endl;
+          g_log<<Logger::Error<<"["<<MT->getTid()<<"] dropping UDP query from "<<source.toString()<<", address not matched by allow-from"<<endl;
         }
 
         g_stats.unauthorizedUDP++;
         return;
       }
+
       BOOST_STATIC_ASSERT(offsetof(sockaddr_in, sin_port) == offsetof(sockaddr_in6, sin6_port));
       if(!fromaddr.sin4.sin_port) { // also works for IPv6
         if(!g_quiet) {
@@ -2553,7 +2716,6 @@ static void handleNewUDPQuestion(int fd, FDMultiplexer::funcparam_t& var)
       }
 
       try {
-        data.resize(static_cast<size_t>(len));
         dnsheader* dh=(dnsheader*)&data[0];
 
         if(dh->qr) {
@@ -2596,13 +2758,16 @@ static void handleNewUDPQuestion(int fd, FDMultiplexer::funcparam_t& var)
               getsockname(fd, (sockaddr*)&dest, &slen); // if this fails, we're ok with it
             }
           }
+          if (!proxyProto) {
+            destination = dest;
+          }
 
           if(g_weDistributeQueries) {
-            distributeAsyncFunction(data, boost::bind(doProcessUDPQuestion, data, fromaddr, dest, tv, fd));
+            distributeAsyncFunction(data, boost::bind(doProcessUDPQuestion, data, fromaddr, dest, source, destination, tv, fd, proxyProtocolValues));
           }
           else {
             ++s_threadInfos[t_id].numberOfDistributedQueries;
-            doProcessUDPQuestion(data, fromaddr, dest, tv, fd);
+            doProcessUDPQuestion(data, fromaddr, dest, source, destination, tv, fd, proxyProtocolValues);
           }
         }
       }
@@ -4060,6 +4225,9 @@ static int serviceMain(int argc, char*argv[])
   g_XPFAcl.toMasks(::arg()["xpf-allow-from"]);
   g_xpfRRCode = ::arg().asNum("xpf-rr-code");
 
+  g_proxyProtocolACL.toMasks(::arg()["proxy-protocol-from"]);
+  g_proxyProtocolMaximumSize = ::arg().asNum("proxy-protocol-maximum-size");
+
   g_networkTimeoutMsec = ::arg().asNum("network-timeout");
 
   g_initialDomainMap = parseAuthAndForwards();
@@ -4782,6 +4950,9 @@ int main(int argc, char **argv)
     ::arg().set("xpf-allow-from","XPF information is only processed from these subnets")="";
     ::arg().set("xpf-rr-code","XPF option code to use")="0";
 
+    ::arg().set("proxy-protocol-from", "A Proxy Protocol header is only allowed from these subnets")="";
+    ::arg().set("proxy-protocol-maximum-size", "The maximum size of a proxy protocol payload, including the TLV values")="512";
+
     ::arg().set("udp-source-port-min", "Minimum UDP port to bind on")="1024";
     ::arg().set("udp-source-port-max", "Maximum UDP port to bind on")="65535";
     ::arg().set("udp-source-port-avoid", "List of comma separated UDP port number to avoid")="11211";
diff --git a/pdns/proxy-protocol.cc b/pdns/proxy-protocol.cc
new file mode 100644 (file)
index 0000000..be635c4
--- /dev/null
@@ -0,0 +1,254 @@
+/*
+ * 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 "proxy-protocol.hh"
+
+// TODO: maybe use structs instead of explicitly working byte by byte, like https://github.com/dovecot/core/blob/master/src/lib-master/master-service-haproxy.c
+
+#define PROXYMAGIC "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A"
+#define PROXYMAGICLEN sizeof(PROXYMAGIC)-1
+
+static string proxymagic(PROXYMAGIC, PROXYMAGICLEN);
+
+static std::string makeSimpleHeader(uint8_t command, uint8_t protocol, uint16_t contentLen)
+{
+  std::string ret;
+  const uint8_t versioncommand = (0x20 | command);
+
+  ret.reserve(proxymagic.size() + sizeof(versioncommand) + sizeof(protocol) + sizeof(contentLen) + contentLen);
+
+  ret.append(proxymagic);
+
+  ret.append(reinterpret_cast<const char*>(&versioncommand), sizeof(versioncommand));
+  ret.append(reinterpret_cast<const char*>(&protocol), sizeof(protocol));
+
+  ret.append(reinterpret_cast<const char*>(&contentLen), sizeof(contentLen));
+
+  return ret;
+}
+
+std::string makeLocalProxyHeader()
+{
+  return makeSimpleHeader(0x00, 0, 0);
+}
+
+std::string makeProxyHeader(bool tcp, const ComboAddress& source, const ComboAddress& destination, const std::vector<ProxyProtocolValue>& values)
+{
+  if (source.sin4.sin_family != destination.sin4.sin_family) {
+    throw std::runtime_error("The PROXY destination and source addresses must be of the same family");
+  }
+
+  const uint8_t command = 0x01;
+  const uint8_t protocol = (source.isIPv4() ? 0x10 : 0x20) | (tcp ? 0x01 : 0x02);
+  const size_t addrSize = source.isIPv4() ? sizeof(source.sin4.sin_addr.s_addr) : sizeof(source.sin6.sin6_addr.s6_addr);
+  const uint16_t sourcePort = source.sin4.sin_port;
+  const uint16_t destinationPort = destination.sin4.sin_port;
+
+  size_t valuesSize = 0;
+  for (const auto& value : values) {
+    if (value.content.size() > std::numeric_limits<uint16_t>::max()) {
+      throw std::runtime_error("The size of proxy protocol values is limited to " + std::to_string(std::numeric_limits<uint16_t>::max()) + ", trying to add a value of size " + std::to_string(value.content.size()));
+    }
+    valuesSize += sizeof(uint8_t) + sizeof(uint8_t) * 2 + value.content.size();
+  }
+
+  size_t total = (addrSize * 2) + sizeof(sourcePort) + sizeof(destinationPort) + valuesSize;
+  if (total > std::numeric_limits<uint16_t>::max()) {
+    throw std::runtime_error("The size of a proxy protocol header is limited to " + std::to_string(std::numeric_limits<uint16_t>::max()) + ", trying to send one of size " + std::to_string(total));
+  }
+
+  const uint16_t contentlen = htons(static_cast<uint16_t>(total));
+  std::string ret = makeSimpleHeader(command, protocol, contentlen);
+
+  // We already established source and destination sin_family equivalence
+  if (source.isIPv4()) {
+    assert(addrSize == sizeof(source.sin4.sin_addr.s_addr));
+    ret.append(reinterpret_cast<const char*>(&source.sin4.sin_addr.s_addr), addrSize);
+    assert(addrSize == sizeof(destination.sin4.sin_addr.s_addr));
+    ret.append(reinterpret_cast<const char*>(&destination.sin4.sin_addr.s_addr), addrSize);
+  }
+  else {
+    assert(addrSize == sizeof(source.sin6.sin6_addr.s6_addr));
+    ret.append(reinterpret_cast<const char*>(&source.sin6.sin6_addr.s6_addr), addrSize);
+    assert(addrSize == sizeof(destination.sin6.sin6_addr.s6_addr));
+    ret.append(reinterpret_cast<const char*>(&destination.sin6.sin6_addr.s6_addr), addrSize);
+  }
+
+  ret.append(reinterpret_cast<const char*>(&sourcePort), sizeof(sourcePort));
+  ret.append(reinterpret_cast<const char*>(&destinationPort), sizeof(destinationPort));
+
+  for (const auto& value : values) {
+    uint16_t contentSize = htons(static_cast<uint16_t>(value.content.size()));
+    ret.append(reinterpret_cast<const char*>(&value.type), sizeof(value.type));
+    ret.append(reinterpret_cast<const char*>(&contentSize), sizeof(contentSize));
+    ret.append(reinterpret_cast<const char*>(value.content.data()), value.content.size());
+  }
+
+  return ret;
+}
+
+/* returns: number of bytes consumed (positive) after successful parse
+         or number of bytes missing (negative)
+         or unfixable parse error (0)*/
+ssize_t isProxyHeaderComplete(const std::string& header, bool* proxy, bool* tcp, size_t* addrSizeOut, uint8_t* protocolOut)
+{
+  static const size_t addr4Size = sizeof(ComboAddress::sin4.sin_addr.s_addr);
+  static const size_t addr6Size = sizeof(ComboAddress::sin6.sin6_addr.s6_addr);
+  size_t addrSize = 0;
+  uint8_t versioncommand;
+  uint8_t protocol;
+
+  if (header.size() < s_proxyProtocolMinimumHeaderSize) {
+    // this is too short to be a complete proxy header
+    return -(s_proxyProtocolMinimumHeaderSize - header.size());
+  }
+
+  if (header.compare(0, proxymagic.size(), proxymagic) != 0) {
+    // wrong magic, can not be a proxy header
+    return 0;
+  }
+
+  versioncommand = header.at(12);
+  /* check version */
+  if (!(versioncommand & 0x20)) {
+    return 0;
+  }
+
+  /* remove the version to get the command */
+  uint8_t command = versioncommand & ~0x20;
+
+  if (command == 0x01) {
+    protocol = header.at(13);
+    if ((protocol & 0xf) == 1) {
+      if (tcp) {
+        *tcp = true;
+      }
+    } else if ((protocol & 0xf) == 2) {
+      if (tcp) {
+        *tcp = false;
+      }
+    } else {
+      return 0;
+    }
+
+    protocol = protocol >> 4;
+
+    if (protocol == 1) {
+      if (protocolOut) {
+        *protocolOut = 4;
+      }
+      addrSize = addr4Size; // IPv4
+    } else if (protocol == 2) {
+      if (protocolOut) {
+        *protocolOut = 6;
+      }
+      addrSize = addr6Size; // IPv6
+    } else {
+      // invalid protocol
+      return 0;
+    }
+
+    if (addrSizeOut) {
+      *addrSizeOut = addrSize;
+    }
+
+    if (proxy) {
+      *proxy = true;
+    }
+  }
+  else if (command == 0x00) {
+    if (proxy) {
+      *proxy = false;
+    }
+  }
+  else {
+    /* unsupported command */
+    return 0;
+  }
+
+  uint16_t contentlen = (static_cast<uint8_t>(header.at(14)) << 8) + static_cast<uint8_t>(header.at(15));
+  uint16_t expectedlen = 0;
+  if (command != 0x00) {
+    expectedlen = (addrSize * 2) + sizeof(ComboAddress::sin4.sin_port) + sizeof(ComboAddress::sin4.sin_port);
+  }
+
+  if (contentlen < expectedlen) {
+    return 0;
+  }
+
+  if (header.size() < s_proxyProtocolMinimumHeaderSize + contentlen) {
+    return -((s_proxyProtocolMinimumHeaderSize + contentlen) - header.size());
+  }
+
+  return s_proxyProtocolMinimumHeaderSize + contentlen;
+}
+
+/* returns: number of bytes consumed (positive) after successful parse
+         or number of bytes missing (negative)
+         or unfixable parse error (0)*/
+ssize_t parseProxyHeader(const std::string& header, bool& proxy, ComboAddress& source, ComboAddress& destination, bool& tcp, std::vector<ProxyProtocolValue>& values)
+{
+  size_t addrSize = 0;
+  uint8_t protocol = 0;
+  ssize_t got = isProxyHeaderComplete(header, &proxy, &tcp, &addrSize, &protocol);
+  if (got <= 0) {
+    return got;
+  }
+
+  size_t pos = s_proxyProtocolMinimumHeaderSize;
+
+  if (proxy) {
+    source = makeComboAddressFromRaw(protocol, &header.at(pos), addrSize);
+    pos = pos + addrSize;
+    destination = makeComboAddressFromRaw(protocol, &header.at(pos), addrSize);
+    pos = pos + addrSize;
+    source.setPort((static_cast<uint8_t>(header.at(pos)) << 8) + static_cast<uint8_t>(header.at(pos+1)));
+    pos = pos + sizeof(uint16_t);
+    destination.setPort((static_cast<uint8_t>(header.at(pos)) << 8) + static_cast<uint8_t>(header.at(pos+1)));
+    pos = pos + sizeof(uint16_t);
+  }
+
+  size_t remaining = got - pos;
+  while (remaining >= (sizeof(uint8_t) + sizeof(uint16_t))) {
+    /* we still have TLV values to parse */
+    uint8_t type = static_cast<uint8_t>(header.at(pos));
+    pos += sizeof(uint8_t);
+    uint16_t len = (static_cast<uint8_t>(header.at(pos)) << 8) + static_cast<uint8_t>(header.at(pos + 1));
+    pos += sizeof(uint16_t);
+
+    if (len > 0) {
+      if (len > (got - pos)) {
+        return 0;
+      }
+
+      values.push_back({ std::string(&header.at(pos), len), type });
+      pos += len;
+    }
+    else {
+      values.push_back({ std::string(), type });
+    }
+
+    remaining = got - pos;
+  }
+
+  return pos;
+}
diff --git a/pdns/proxy-protocol.hh b/pdns/proxy-protocol.hh
new file mode 100644 (file)
index 0000000..66b8f0a
--- /dev/null
@@ -0,0 +1,46 @@
+/*
+ * 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>
+
+struct ProxyProtocolValue
+{
+  std::string content;
+  uint8_t type;
+};
+
+static const size_t s_proxyProtocolMinimumHeaderSize = 16;
+
+std::string makeLocalProxyHeader();
+std::string makeProxyHeader(bool tcp, const ComboAddress& source, const ComboAddress& destination, const std::vector<ProxyProtocolValue>& values);
+
+/* returns: number of bytes consumed (positive) after successful parse
+         or number of bytes missing (negative)
+         or unfixable parse error (0)*/
+ssize_t isProxyHeaderComplete(const std::string& header, bool* proxy=nullptr, bool* tcp=nullptr, size_t* addrSizeOut=nullptr, uint8_t* protocolOut=nullptr);
+
+/* returns: number of bytes consumed (positive) after successful parse
+         or number of bytes missing (negative)
+         or unfixable parse error (0)*/
+ssize_t parseProxyHeader(const std::string& payload, bool& proxy, ComboAddress& source, ComboAddress& destination, bool& tcp, std::vector<ProxyProtocolValue>& values);
index d48c5708872b61f87f662f7ce30d5a2d731f8761..596741098486e302e78b6b0e2540acfb142c38c3 100644 (file)
@@ -116,6 +116,7 @@ static const oid variableResponsesOID[] = { RECURSOR_STATS_OID, 97 };
 static const oid specialMemoryUsageOID[] = { RECURSOR_STATS_OID, 98 };
 static const oid rebalancedQueriesOID[] = { RECURSOR_STATS_OID, 99 };
 static const oid qnameMinFallbackSuccessOID[] = { RECURSOR_STATS_OID, 100 };
+static const oid proxyProtocolInvalidOID[] = { RECURSOR_STATS_OID, 101 };
 
 static std::unordered_map<oid, std::string> s_statsMap;
 
@@ -325,5 +326,6 @@ RecursorSNMPAgent::RecursorSNMPAgent(const std::string& name, const std::string&
   registerCounter64Stat("policy-result-custom", policyResultCustomOID, OID_LENGTH(policyResultCustomOID));
   registerCounter64Stat("special-memory-usage", specialMemoryUsageOID, OID_LENGTH(specialMemoryUsageOID));
   registerCounter64Stat("rebalanced-queries", rebalancedQueriesOID, OID_LENGTH(rebalancedQueriesOID));
+  registerCounter64Stat("proxy-protocol-invalid", proxyProtocolInvalidOID, OID_LENGTH(proxyProtocolInvalidOID));
 #endif /* HAVE_NET_SNMP */
 }
index 8829be099d969dcf1c78c089492b193c2f0bab44..6f964fda06f6368f36b0a331fa4fe680e900a22c 100644 (file)
@@ -1172,6 +1172,8 @@ void registerAllStats()
 
   addGetStat("rebalanced-queries", &g_stats.rebalancedQueries);
 
+  addGetStat("proxy-protocol-invalid", &g_stats.proxyProtocolInvalidCount);
+
   /* make sure that the ECS stats are properly initialized */
   SyncRes::clearECSStats();
   for (size_t idx = 0; idx < SyncRes::s_ecsResponsesBySubnetSize4.size(); idx++) {
index 20105c0e2561bce6057503c689d34eddc8cf9efe..657ea7eec6bcb4f35d98d99d675195018d60e2a5 100644 (file)
@@ -144,6 +144,7 @@ pdns_recursor_SOURCES = \
        pdnsexception.hh \
        pollmplexer.cc \
        protobuf.cc protobuf.hh \
+       proxy-protocol.cc proxy-protocol.hh \
        pubsuffix.hh pubsuffix.cc \
        pubsuffixloader.cc \
        qtype.hh qtype.cc \
index d4d70e5cbf48e0212572f5cfbd5fdc355e740edd..0155b5ddaa1fd954dd5ecafc8458b2caeecca4b6 100644 (file)
@@ -15,7 +15,7 @@ IMPORTS
         FROM SNMPv2-CONF;
 
 rec MODULE-IDENTITY
-    LAST-UPDATED "201911140000Z"
+    LAST-UPDATED "202002170000Z"
     ORGANIZATION "PowerDNS BV"
     CONTACT-INFO "support@powerdns.com"
     DESCRIPTION
@@ -30,6 +30,9 @@ rec MODULE-IDENTITY
     REVISION "201911140000Z"
     DESCRIPTION "Added qnameMinFallbackSuccess stats."
 
+    REVISION "202002170000Z"
+    DESCRIPTION "Added proxyProtocolInvalid metric."
+
     ::= { powerdns 2 }
 
 powerdns               OBJECT IDENTIFIER ::= { enterprises 43315 }
@@ -836,6 +839,14 @@ qnameMinFallbackSuccess OBJECT-TYPE
         "Number of successful queries due to fallback mechanism within 'qname-minimization' setting"
     ::= { stats 100 }
 
+proxyProtocolInvalid OBJECT-TYPE
+    SYNTAX Counter64
+    MAX-ACCESS read-only
+    STATUS current
+    DESCRIPTION
+        "Number of invalid proxy protocol headers received"
+    ::= { stats 101 }
+
 ---
 --- Traps / Notifications
 ---
@@ -979,7 +990,8 @@ recGroup OBJECT-GROUP
         specialMemoryUsage,
         rebalancedQueries,
         trapReason,
-        qnameMinFallbackSuccess
+        qnameMinFallbackSuccess,
+        proxyProtocolInvalid
     }
     STATUS current
     DESCRIPTION "Objects conformance group for PowerDNS Recursor"
index 704c44149df78d370a7901928b9512dba28ad177..25ca8d8cc836bf12dec315965331089383031cdc 100644 (file)
@@ -193,6 +193,12 @@ The DNSQuestion object contains at least the following fields:
 
       Returns the :class:`DNSHeader` of the query or nil.
 
+  .. method:: DNSQuestion:getProxyProtocolValues() -> {ProxyProtocolValue}
+
+    .. versionadded:: 4.4.0
+
+      Get the Proxy Protocol Type-Length Values if any, as a table of  :class:`ProxyProtocolValue` objects.
+
   .. method:: DNSQuestion:getRecords() -> {DNSRecord}
 
       Get a table of DNS Records in this DNS Question (or answer by now).
@@ -290,3 +296,20 @@ The EDNSOptionView Class
   .. method:: EDNSOptionView:getContent()
 
     Returns a NULL-safe string object of the first value of this EDNS option.
+
+The ProxyProtocolValue Class
+============================
+
+.. class:: ProxyProtocolValue
+
+  .. versionadded:: 4.4.0
+
+  An object that represents the value of a Proxy Protocol Type-Length Value
+
+  .. method:: ProxyProtocolValue:getContent() -> str
+
+    Returns a NULL-safe string object.
+
+  .. method:: ProxyProtocolValue:getType() -> int
+
+    Returns the type of this value.
index 7a5781c198b564aaa9da5bb4d284883002dd4b4a..08f48bb99fe3604ece466806d89f065dd63bb797 100644 (file)
@@ -53,13 +53,18 @@ Interception Functions
     :param DNSHeader dh: The DNS Header of the query.
 
 
-.. function:: gettag(remote, ednssubnet, localip, qname, qtype, ednsoptions, tcp) -> int
+.. function:: gettag(remote, ednssubnet, localip, qname, qtype, ednsoptions, tcp, proxyprotocolvalues) -> int
+              gettag(remote, ednssubnet, localip, qname, qtype, ednsoptions, tcp) -> int
               gettag(remote, ednssubnet, localip, qname, qtype, ednsoptions) -> int
 
     .. versionchanged:: 4.1.0
 
       The ``tcp`` parameter was added.
 
+    .. versionchanged:: 4.4.0
+
+      The ``proxyprotocolvalues`` parameter was added.
+
     The ``gettag`` function is invoked when the Recursor attempts to discover in which packetcache an answer is available.
 
     This function must return an integer, which is the tag number of the packetcache.
@@ -69,6 +74,7 @@ Interception Functions
     .. versionadded:: 4.1.0
 
         It can also return a table whose keys and values are strings to fill the :attr:`DNSQuestion.data` table, as well as a ``requestorId`` value to fill the :attr:`DNSQuestion.requestorId` field and a ``deviceId`` value to fill the :attr:`DNSQuestion.deviceId` field.
+
     .. versionadded:: 4.3.0
 
         Along the ``deviceId`` value that can be returned, it was addded a ``deviceName`` field to fill the :attr:`DNSQuestion.deviceName` field.
@@ -84,6 +90,7 @@ Interception Functions
     :param int qtype: The query type of the query
     :param ednsoptions: A table whose keys are EDNS option codes and values are :class:`EDNSOptionView` objects. This table is empty unless the :ref:`setting-gettag-needs-edns-options` option is set.
     :param bool tcp: Added in 4.1.0, a boolean indicating whether the query was received over UDP (false) or TCP (true).
+    :param proxyprotocolvalues: Added in 4.4.0, a table of :class:`ProxyProtocolValue` objects representing the Type-Length Values received via the Proxy Protocol, if any.
 
 .. function:: prerpz(dq)
 
index e6aedde774bafcf7df86caeef83358244b6d3657..705d13b6a7480530dd9a338b2c4bd09588c99fab 100644 (file)
@@ -406,6 +406,12 @@ policy-result-custom
 ^^^^^^^^^^^^^^^^^^^^
 packets that were sent a custom answer by   the RPZ/filter engine
 
+proxy-protocol-invalid
+^^^^^^^^^^^^^^^^^^^^^^
+.. versionadded:: 4.4
+
+Invalid proxy-protocol headers received.
+
 qa-latency
 ^^^^^^^^^^
 shows the current latency average, in microseconds,   exponentially weighted over past 'latency-statistic-size' packets
index cbebe9f2ef239d9580ae5c597d4d5f12b8eaf937..fffda3d167cbe66b615d764ebeb8f1b5ea827412 100644 (file)
@@ -24,6 +24,8 @@ The default allows access only from :rfc:`1918` private IP addresses.
 Due to the aggressive nature of the internet these days, it is highly recommended to not open up the recursor for the entire internet.
 Questions 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.
+
 .. _setting-allow-from-file:
 
 ``allow-from-file``
@@ -1226,6 +1228,31 @@ Improves performance on Linux.
 
 Whether to compute the latency of responses in protobuf messages using the timestamp set by the kernel when the query packet was received (when available), instead of computing it based on the moment we start processing the query.
 
+.. _setting-proxy-protocol-from:
+
+``proxy-protocol-from``
+-----------------------
+.. versionadded:: 4.4.0
+
+-  IP ranges, separated by commas
+-  Default: empty
+
+Ranges that are required to send a Proxy Protocol header in front of UDP and TCP queries, to pass the original source and destination addresses and ports to the recursor, as well as custom values.
+Queries that are not prefixed with such a header will not be accepted from clients in these ranges. Queries prefixed by headers from clients that are not listed in these ranges will be dropped.
+
+Note that once a Proxy Protocol header has been received, the source address from the proxy header instead of the address of the proxy will be checked against the `allow-from`_ ACL, 
+
+.. _setting-proxy-protocol-maximum-size:
+
+``proxy-protocol-maximum-size``
+-------------------------------
+.. versionadded:: 4.4.0
+
+-  Integer
+-  Default: 512
+
+The maximum size, in bytes, of a Proxy Protocol payload (header, addresses and ports, and TLV values). Queries with a larger payload will be dropped.
+
 .. _setting-public-suffix-list-file:
 
 ``public-suffix-list-file``
diff --git a/pdns/recursordist/proxy-protocol.cc b/pdns/recursordist/proxy-protocol.cc
new file mode 120000 (symlink)
index 0000000..ae6a943
--- /dev/null
@@ -0,0 +1 @@
+../proxy-protocol.cc
\ No newline at end of file
diff --git a/pdns/recursordist/proxy-protocol.hh b/pdns/recursordist/proxy-protocol.hh
new file mode 120000 (symlink)
index 0000000..bc45ee8
--- /dev/null
@@ -0,0 +1 @@
+../proxy-protocol.hh
\ No newline at end of file
index 1279d91ffe599674ddbac8f143fca127204d9823..c1baff02d662a11bcf97f8739226debf0dee1a87 100644 (file)
@@ -7,6 +7,7 @@
 #include "ednsoptions.hh"
 #include "ednssubnet.hh"
 #include "misc.hh"
+#include "proxy-protocol.hh"
 #include "sstuff.hh"
 #include "statbag.hh"
 #include <boost/array.hpp>
@@ -17,9 +18,9 @@
 
 StatBag S;
 
-bool hidettl = false;
+static bool hidettl = false;
 
-string ttl(uint32_t ttl)
+static string ttl(uint32_t ttl)
 {
   if (hidettl)
     return "[ttl]";
@@ -27,12 +28,13 @@ string ttl(uint32_t ttl)
     return std::to_string(ttl);
 }
 
-void usage()
+static void usage()
 {
   cerr << "sdig" << endl;
   cerr << "Syntax: sdig IP-ADDRESS-OR-DOH-URL PORT QNAME QTYPE "
           "[dnssec] [ednssubnet SUBNET/MASK] [hidesoadetails] [hidettl] "
-          "[recurse] [showflags] [tcp] [xpf XPFDATA] [class CLASSNUM]"
+          "[recurse] [showflags] [tcp] [xpf XPFDATA] [class CLASSNUM] "
+          "[proxy UDP(0)/TCP(1) SOURCE-IP-ADDRESS-AND-PORT DESTINATION-IP-ADDRESS-AND-PORT]"
        << endl;
 }
 
@@ -195,6 +197,7 @@ try {
   uint16_t xpfcode = 0, xpfversion = 0, xpfproto = 0;
   char *xpfsrc = NULL, *xpfdst = NULL;
   uint16_t qclass = QClass::IN;
+  string proxyheader;
 
   for (int i = 1; i < argc; i++) {
     if ((string)argv[i] == "--help") {
@@ -254,6 +257,16 @@ try {
         }
         qclass = atoi(argv[++i]);
       }
+      if (strcmp(argv[i], "proxy") == 0) {
+        if(argc < i+4) {
+          cerr<<"proxy needs three arguments"<<endl;
+          exit(EXIT_FAILURE);
+        }
+        bool ptcp = atoi(argv[++i]);
+        ComboAddress src(argv[++i]);
+        ComboAddress dest(argv[++i]);
+        proxyheader = makeProxyHeader(ptcp, src, dest, {});
+      }
     }
   }
 
@@ -295,6 +308,7 @@ try {
     mch.insert(std::make_pair("Content-Type", "application/dns-message"));
     mch.insert(std::make_pair("Accept", "application/dns-message"));
     string question(packet.begin(), packet.end());
+    // FIXME: how do we use proxyheader here?
     reply = mc.postURL(argv[1], question, mch);
     printReply(reply, showflags, hidesoadetails);
 #else
@@ -303,10 +317,26 @@ try {
   } else if (fromstdin) {
     std::istreambuf_iterator<char> begin(std::cin), end;
     reply = string(begin, end);
+
+    ComboAddress source, destination;
+    bool wastcp;
+    bool proxy = false;
+    std::vector<ProxyProtocolValue> ignoredValues;
+    ssize_t offset = parseProxyHeader(reply, proxy, source, destination, wastcp, ignoredValues);
+    if (offset && proxy) {
+      cout<<"proxy "<<(wastcp ? "tcp" : "udp")<<" headersize="<<offset<<" source="<<source.toStringWithPort()<<" destination="<<destination.toStringWithPort()<<endl;
+      reply = reply.substr(offset);
+    }
+
+    if (tcp) {
+      reply = reply.substr(2);
+    }
+
     printReply(reply, showflags, hidesoadetails);
   } else if (tcp) {
     Socket sock(dest.sin4.sin_family, SOCK_STREAM);
     sock.connect(dest);
+    sock.writen(proxyheader);
     for (const auto& it : questions) {
       vector<uint8_t> packet;
       fillPacket(packet, it.first, it.second, dnssec, ednsnm, recurse, xpfcode,
@@ -345,6 +375,7 @@ try {
       xpfproto, xpfsrc, xpfdst, qclass);
     string question(packet.begin(), packet.end());
     Socket sock(dest.sin4.sin_family, SOCK_DGRAM);
+    question = proxyheader + question;
     sock.sendTo(question, dest);
     int result = waitForData(sock.getHandle(), 10);
     if (result < 0)
index b8021447d7ac9ec90719f94de515d3a9243db2b3..473f5b5392f990e42cf7253d2438ba1f3cc51c43 100644 (file)
@@ -49,6 +49,7 @@
 #include "ednssubnet.hh"
 #include "filterpo.hh"
 #include "negcache.hh"
+#include "proxy-protocol.hh"
 #include "sholder.hh"
 
 #ifdef HAVE_CONFIG_H
@@ -1018,6 +1019,7 @@ struct RecursorStats
   std::map<vState, std::atomic<uint64_t> > dnssecResults;
   std::map<DNSFilterEngine::PolicyKind, std::atomic<uint64_t> > policyResults;
   std::atomic<uint64_t> rebalancedQueries{0};
+  std::atomic<uint64_t> proxyProtocolInvalidCount{0};
 };
 
 //! represents a running TCP/IP client session
@@ -1032,10 +1034,15 @@ public:
     return d_fd;
   }
 
+  std::vector<ProxyProtocolValue> proxyProtocolValues;
   std::string data;
   const ComboAddress d_remote;
+  ComboAddress d_source;
+  ComboAddress d_destination;
   size_t queriesCount{0};
-  enum stateenum {BYTE0, BYTE1, GETQUESTION, DONE} state{BYTE0};
+  size_t proxyProtocolGot{0};
+  ssize_t proxyProtocolNeed{0};
+  enum stateenum {PROXYPROTOCOLHEADER, BYTE0, BYTE1, GETQUESTION, DONE} state{BYTE0};
   uint16_t qlen{0};
   uint16_t bytesread{0};
   uint16_t d_requestsInFlight{0}; // number of mthreads spawned for this connection
diff --git a/pdns/test-proxy_protocol_cc.cc b/pdns/test-proxy_protocol_cc.cc
new file mode 100644 (file)
index 0000000..e61f978
--- /dev/null
@@ -0,0 +1,227 @@
+#define BOOST_TEST_DYN_LINK
+#define BOOST_TEST_NO_MAIN
+#include <boost/test/unit_test.hpp>
+
+#include "iputils.hh"
+#include "proxy-protocol.hh"
+
+using namespace boost;
+using std::string;
+
+
+BOOST_AUTO_TEST_SUITE(test_proxy_protocol_cc)
+
+#define BINARY(s) (std::string(s, sizeof(s) - 1))
+
+#define PROXYMAGIC "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A"
+#define PROXYMAGICLEN sizeof(PROXYMAGIC)-1
+
+static string proxymagic(PROXYMAGIC, PROXYMAGICLEN);
+
+BOOST_AUTO_TEST_CASE(test_roundtrip) {
+  std::vector<ProxyProtocolValue> values;
+  string proxyheader;
+
+  bool ptcp = true;
+  ComboAddress src("65.66.67.68:18762");  // 18762 = 0x494a = "IJ"
+  ComboAddress dest("69.70.71.72:19276"); // 19276 = 0x4b4c = "KL"
+  proxyheader = makeProxyHeader(ptcp, src, dest, values);
+
+  BOOST_CHECK_EQUAL(proxyheader, BINARY(
+    PROXYMAGIC
+    "\x21"          // version | command
+    "\x11"          // ipv4=0x10 | TCP=0x1
+    "\x00\x0c"      // 4 bytes IPv4 * 2 + 2 port numbers = 8 + 2 * 2 =12 = 0xc
+    "ABCD"          // 65.66.67.68
+    "EFGH"          // 69.70.71.72
+    "IJ"            // src port
+    "KL"            // dst port
+    ));
+
+  bool proxy;
+  bool ptcp2;
+  ComboAddress src2, dest2;
+
+  BOOST_CHECK_EQUAL(parseProxyHeader(proxyheader, proxy, src2, dest2, ptcp2, values), 28);
+
+  BOOST_CHECK_EQUAL(proxy, true);
+  BOOST_CHECK_EQUAL(ptcp2, true);
+  BOOST_CHECK(src2 == src);
+  BOOST_CHECK(dest2 == dest);
+}
+
+BOOST_AUTO_TEST_CASE(test_local_proxy_header) {
+  auto payload = makeLocalProxyHeader();
+
+  BOOST_CHECK_EQUAL(payload, BINARY(
+    PROXYMAGIC
+    "\x20"          // version | command
+    "\x00"          // protocol family and address are set to 0
+    "\x00\x00"      // no content
+    ));
+
+  bool proxy;
+  bool tcp = false;
+  ComboAddress src, dest;
+  std::vector<ProxyProtocolValue> values;
+
+  BOOST_CHECK_EQUAL(parseProxyHeader(payload, proxy, src, dest, tcp, values), 16);
+
+  BOOST_CHECK_EQUAL(proxy, false);
+  BOOST_CHECK_EQUAL(tcp, false);
+  BOOST_CHECK_EQUAL(values.size(), 0U);
+}
+
+BOOST_AUTO_TEST_CASE(test_tlv_values_content_len_signedness) {
+  std::string largeValue;
+  /* this value will make the content length parsing fail in case of signedness mistake */
+  largeValue.resize(65128, 'A');
+  const std::vector<ProxyProtocolValue> values = { { "foo", 0 }, { largeValue, 255 }};
+
+  const bool tcp = false;
+  const ComboAddress src("[2001:db8::1]:0");
+  const ComboAddress dest("[::1]:65535");
+  const auto payload = makeProxyHeader(tcp, src, dest, values);
+
+  bool proxy;
+  bool tcp2;
+  ComboAddress src2;
+  ComboAddress dest2;
+  std::vector<ProxyProtocolValue> parsedValues;
+
+  BOOST_CHECK_EQUAL(parseProxyHeader(payload, proxy, src2, dest2, tcp2, parsedValues), 16 + 36 + 6 + 65131);
+  BOOST_CHECK_EQUAL(proxy, true);
+  BOOST_CHECK_EQUAL(tcp2, tcp);
+  BOOST_CHECK(src2 == src);
+  BOOST_CHECK(dest2 == dest);
+  BOOST_REQUIRE_EQUAL(parsedValues.size(), values.size());
+  for (size_t idx = 0; idx < values.size(); idx++) {
+    BOOST_CHECK_EQUAL(parsedValues.at(idx).type, values.at(idx).type);
+    BOOST_CHECK_EQUAL(parsedValues.at(idx).content, values.at(idx).content);
+  }
+}
+
+BOOST_AUTO_TEST_CASE(test_tlv_values_length_signedness) {
+  std::string largeValue;
+  /* this value will make the TLV length parsing fail in case of signedness mistake */
+  largeValue.resize(65000, 'A');
+  const std::vector<ProxyProtocolValue> values = { { "foo", 0 }, { largeValue, 255 }};
+
+  const bool tcp = false;
+  const ComboAddress src("[2001:db8::1]:0");
+  const ComboAddress dest("[::1]:65535");
+  const auto payload = makeProxyHeader(tcp, src, dest, values);
+
+  bool proxy;
+  bool tcp2;
+  ComboAddress src2;
+  ComboAddress dest2;
+  std::vector<ProxyProtocolValue> parsedValues;
+
+  BOOST_CHECK_EQUAL(parseProxyHeader(payload, proxy, src2, dest2, tcp2, parsedValues), 16 + 36 + 6 + 65003);
+  BOOST_CHECK_EQUAL(proxy, true);
+  BOOST_CHECK_EQUAL(tcp2, tcp);
+  BOOST_CHECK(src2 == src);
+  BOOST_CHECK(dest2 == dest);
+  BOOST_REQUIRE_EQUAL(parsedValues.size(), values.size());
+  for (size_t idx = 0; idx < values.size(); idx++) {
+    BOOST_CHECK_EQUAL(parsedValues.at(idx).type, values.at(idx).type);
+    BOOST_CHECK_EQUAL(parsedValues.at(idx).content, values.at(idx).content);
+  }
+}
+
+BOOST_AUTO_TEST_CASE(test_parsing_invalid_headers) {
+  const std::vector<ProxyProtocolValue> noValues;
+
+  const bool tcp = false;
+  const ComboAddress src("[2001:db8::1]:0");
+  const ComboAddress dest("[::1]:65535");
+  const auto payload = makeProxyHeader(tcp, src, dest, noValues);
+
+  bool proxy;
+  bool tcp2;
+  ComboAddress src2;
+  ComboAddress dest2;
+  std::vector<ProxyProtocolValue> values;
+
+  {
+    /* just checking that everything works */
+    BOOST_CHECK_EQUAL(parseProxyHeader(payload, proxy, src2, dest2, tcp2, values), 52);
+    BOOST_CHECK_EQUAL(proxy, true);
+    BOOST_CHECK_EQUAL(tcp2, tcp);
+    BOOST_CHECK(src2 == src);
+    BOOST_CHECK(dest2 == dest);
+    BOOST_CHECK_EQUAL(values.size(), 0U);
+  }
+
+  {
+    /* too short (not even full header) */
+    std::string truncated = payload;
+    truncated.resize(15);
+    BOOST_CHECK_EQUAL(parseProxyHeader(truncated, proxy, src2, dest2, tcp2, values), -1);
+  }
+
+  {
+    /* too short (missing address part) */
+    std::string truncated = payload;
+    truncated.resize(/* full header */ 16 + /* two IPv6s + port */ 36 - /* truncation */ 1);
+    BOOST_CHECK_EQUAL(parseProxyHeader(truncated, proxy, src2, dest2, tcp2, values), -1);
+  }
+
+  {
+    /* too short (missing TLV) */
+    values = { { "foo", 0 }, { "bar", 255 }} ;
+    const auto payloadWithValues = makeProxyHeader(tcp, src, dest, values);
+
+    std::string truncated = payloadWithValues;
+    truncated.resize(/* full header */ 16 + /* two IPv6s + port */ 36 + /* TLV 1 */ 6 + /* TLV 2 */ 6 - /* truncation */ 2);
+    BOOST_CHECK_EQUAL(parseProxyHeader(truncated, proxy, src2, dest2, tcp2, values), -2);
+  }
+
+  {
+    /* invalid magic */
+    std::string invalid = payload;
+    invalid.at(4) = 42;
+    BOOST_CHECK_EQUAL(parseProxyHeader(invalid, proxy, src2, dest2, tcp2, values), 0);
+  }
+
+  {
+    /* invalid version */
+    std::string invalid = payload;
+    invalid.at(12) = 0x10 | 0x01;
+    BOOST_CHECK_EQUAL(parseProxyHeader(invalid, proxy, src2, dest2, tcp2, values), 0);
+  }
+
+  {
+    /* invalid command */
+    std::string invalid = payload;
+    invalid.at(12) = 0x20 | 0x02;
+    BOOST_CHECK_EQUAL(parseProxyHeader(invalid, proxy, src2, dest2, tcp2, values), 0);
+  }
+
+  {
+    /* invalid family */
+    std::string invalid = payload;
+    invalid.at(13) = (0x04 << 4) | 0x01 /* STREAM */;
+    BOOST_CHECK_EQUAL(parseProxyHeader(invalid, proxy, src2, dest2, tcp2, values), 0);
+  }
+
+  {
+    /* invalid address */
+    std::string invalid = payload;
+    invalid.at(13) = (0x02 /* AF_INET */ << 4) | 0x03;
+    BOOST_CHECK_EQUAL(parseProxyHeader(invalid, proxy, src2, dest2, tcp2, values), 0);
+  }
+
+  {
+    /* TLV advertised len gets out of bounds */
+    values = { { "foo", 0 }, { "bar", 255 }} ;
+    const auto payloadWithValues = makeProxyHeader(tcp, src, dest, values);
+    std::string invalid = payloadWithValues;
+    /* full header (16) + two IPv6s + port (36) + TLV (6) TLV 2 (6) */
+    invalid.at(59) += 1;
+    BOOST_CHECK_EQUAL(parseProxyHeader(invalid, proxy, src2, dest2, tcp2, values), 0);
+  }
+}
+
+BOOST_AUTO_TEST_SUITE_END()
diff --git a/regression-tests.common/proxyprotocol.py b/regression-tests.common/proxyprotocol.py
new file mode 100644 (file)
index 0000000..0677b0d
--- /dev/null
@@ -0,0 +1,193 @@
+#!/usr/bin/env python
+
+import copy
+import socket
+import struct
+
+class ProxyProtocol(object):
+    MAGIC = b'\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A'
+    # Header is magic + versioncommand (1) + family (1) + content length (2)
+    HEADER_SIZE = len(MAGIC) + 1 + 1 + 2
+    PORT_SIZE = 2
+
+    def consumed(self):
+        return self.offset
+
+    def parseHeader(self, data):
+        if len(data) < self.HEADER_SIZE:
+            return False
+
+        if data[:len(self.MAGIC)] != self.MAGIC:
+            return False
+
+        value = struct.unpack('!B', bytes(bytearray([data[12]])))[0]
+        self.version = value >> 4
+        if self.version != 0x02:
+            return False
+
+        self.command = value & ~0x20
+        self.local = False
+        self.offset = self.HEADER_SIZE
+
+        if self.command == 0x00:
+            self.local = True
+        elif self.command == 0x01:
+            value = struct.unpack('!B', bytes(bytearray([data[13]])))[0]
+            self.family = value >> 4
+            if self.family == 0x01:
+                self.addrSize = 4
+            elif self.family == 0x02:
+                self.addrSize = 16
+            else:
+                return False
+
+            self.protocol = value & ~0xF0
+            if self.protocol == 0x01:
+                self.tcp = True
+            elif self.protocol == 0x02:
+                self.tcp = False
+            else:
+                return False
+        else:
+            return False
+
+        self.contentLen = struct.unpack("!H", data[14:16])[0]
+
+        if not self.local:
+            if self.contentLen < (self.addrSize * 2 + self.PORT_SIZE * 2):
+                return False
+
+        return True
+
+    def getAddr(self, data):
+        if len(data) < (self.consumed() + self.addrSize):
+            return False
+
+        value = None
+        if self.family == 0x01:
+            value = socket.inet_ntop(socket.AF_INET, data[self.offset:self.offset + self.addrSize])
+        else:
+            value = socket.inet_ntop(socket.AF_INET6, data[self.offset:self.offset + self.addrSize])
+
+        self.offset = self.offset + self.addrSize
+        return value
+
+    def getPort(self, data):
+        if len(data) < (self.consumed() + self.PORT_SIZE):
+            return False
+
+        value = struct.unpack('!H', data[self.offset:self.offset + self.PORT_SIZE])[0]
+        self.offset = self.offset + self.PORT_SIZE
+        return value
+
+    def parseAddressesAndPorts(self, data):
+        if self.local:
+            return True
+
+        if len(data) < (self.consumed() + self.addrSize * 2 + self.PORT_SIZE * 2):
+            return False
+
+        self.source = self.getAddr(data)
+        self.destination = self.getAddr(data)
+        self.sourcePort = self.getPort(data)
+        self.destinationPort = self.getPort(data)
+        return True
+
+    def parseAdditionalValues(self, data):
+        self.values = []
+        if self.local:
+            return True
+
+        if len(data) < (self.HEADER_SIZE + self.contentLen):
+            return False
+
+        remaining = self.HEADER_SIZE + self.contentLen - self.consumed()
+        if len(data) < remaining:
+            return False
+
+        while remaining >= 3:
+            valueType = struct.unpack("!B", bytes(bytearray([data[self.offset]])))[0]
+            self.offset = self.offset + 1
+            valueLen = struct.unpack("!H", data[self.offset:self.offset+2])[0]
+            self.offset = self.offset + 2
+
+            remaining = remaining - 3
+            if valueLen > 0:
+                if valueLen > remaining:
+                    return False
+                self.values.append([valueType, data[self.offset:self.offset+valueLen]])
+                self.offset = self.offset + valueLen
+                remaining = remaining - valueLen
+
+            else:
+                self.values.append([valueType, ""])
+
+        return True
+
+    @classmethod
+    def getPayload(cls, local, tcp, v6, source, destination, sourcePort, destinationPort, values):
+        payload = copy.deepcopy(cls.MAGIC)
+        version = 0x02
+
+        if local:
+            command = 0x00
+        else:
+            command = 0x01
+
+        value = struct.pack('!B', (version << 4) + command)
+        payload = payload + value
+
+        addrSize = 0
+        family = 0x00
+        protocol = 0x00
+        if not local:
+            if tcp:
+                protocol = 0x01
+            else:
+                protocol = 0x02
+            # sorry but compatibility with python 2 is awful for this,
+            # not going to waste time on it
+            if not v6:
+                family = 0x01
+                addrSize = 4
+            else:
+                family = 0x02
+                addrSize = 16
+
+        value = struct.pack('!B', (family << 4)  + protocol)
+        payload = payload + value
+
+        contentSize = 0
+        if not local:
+            contentSize = contentSize + addrSize * 2 + cls.PORT_SIZE *2
+
+        valuesSize = 0
+        for value in values:
+            valuesSize = valuesSize + 3 + len(value[1])
+
+        contentSize = contentSize + valuesSize
+
+        value = struct.pack('!H', contentSize)
+        payload = payload +  value
+
+        if not local:
+            if family == 0x01:
+                af = socket.AF_INET
+            else:
+                af = socket.AF_INET6
+
+            value = socket.inet_pton(af, source)
+            payload = payload + value
+            value = socket.inet_pton(af, destination)
+            payload = payload + value
+            value = struct.pack('!H', sourcePort)
+            payload = payload + value
+            value = struct.pack('!H', destinationPort)
+            payload = payload + value
+
+        for value in values:
+            valueType = struct.pack('!B', value[0])
+            valueLen = struct.pack('!H', len(value[1]))
+            payload = payload + valueType + valueLen + value[1]
+
+        return payload
diff --git a/regression-tests.dnsdist/proxyprotocol.py b/regression-tests.dnsdist/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.dnsdist/test_ProxyProtocol.py b/regression-tests.dnsdist/test_ProxyProtocol.py
new file mode 100644 (file)
index 0000000..ab6c1a2
--- /dev/null
@@ -0,0 +1,408 @@
+#!/usr/bin/env python
+
+import dns
+import socket
+import struct
+import sys
+import threading
+
+from dnsdisttests import DNSDistTest
+from proxyprotocol import ProxyProtocol
+
+# Python2/3 compatibility hacks
+try:
+  from queue import Queue
+except ImportError:
+  from Queue import Queue
+
+def ProxyProtocolUDPResponder(port, fromQueue, toQueue):
+    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
+    try:
+        sock.bind(("127.0.0.1", port))
+    except socket.error as e:
+        print("Error binding in the Proxy Protocol UDP responder: %s" % str(e))
+        sys.exit(1)
+
+    while True:
+        data, addr = sock.recvfrom(4096)
+
+        proxy = ProxyProtocol()
+        if len(data) < proxy.HEADER_SIZE:
+            continue
+
+        if not proxy.parseHeader(data):
+            continue
+
+        if proxy.local:
+            # likely a healthcheck
+            data = data[proxy.HEADER_SIZE:]
+            request = dns.message.from_wire(data)
+            response = dns.message.make_response(request)
+            wire = response.to_wire()
+            sock.settimeout(2.0)
+            sock.sendto(wire, addr)
+            sock.settimeout(None)
+
+            continue
+
+        payload = data[:(proxy.HEADER_SIZE + proxy.contentLen)]
+        dnsData = data[(proxy.HEADER_SIZE + proxy.contentLen):]
+        toQueue.put([payload, dnsData], True, 2.0)
+        # computing the correct ID for the response
+        request = dns.message.from_wire(dnsData)
+        response = fromQueue.get(True, 2.0)
+        response.id = request.id
+
+        sock.settimeout(2.0)
+        sock.sendto(response.to_wire(), addr)
+        sock.settimeout(None)
+
+    sock.close()
+
+def ProxyProtocolTCPResponder(port, fromQueue, toQueue):
+    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
+    sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
+    try:
+        sock.bind(("127.0.0.1", port))
+    except socket.error as e:
+        print("Error binding in the TCP responder: %s" % str(e))
+        sys.exit(1)
+
+    sock.listen(100)
+    while True:
+        (conn, _) = sock.accept()
+        conn.settimeout(5.0)
+        # try to read the entire Proxy Protocol header
+        proxy = ProxyProtocol()
+        header = conn.recv(proxy.HEADER_SIZE)
+        if not header:
+            conn.close()
+            continue
+
+        if not proxy.parseHeader(header):
+            conn.close()
+            continue
+
+        proxyContent = conn.recv(proxy.contentLen)
+        if not proxyContent:
+            conn.close()
+            continue
+
+        payload = header + proxyContent
+        while True:
+          try:
+            data = conn.recv(2)
+          except socket.timeout:
+            data = None
+
+          if not data:
+            conn.close()
+            break
+
+          (datalen,) = struct.unpack("!H", data)
+          data = conn.recv(datalen)
+
+          toQueue.put([payload, data], True, 2.0)
+
+          response = fromQueue.get(True, 2.0)
+          if not response:
+            conn.close()
+            break
+
+          # computing the correct ID for the response
+          request = dns.message.from_wire(data)
+          response.id = request.id
+
+          wire = response.to_wire()
+          conn.send(struct.pack("!H", len(wire)))
+          conn.send(wire)
+
+        conn.close()
+
+    sock.close()
+
+toProxyQueue = Queue()
+fromProxyQueue = Queue()
+proxyResponderPort = 5470
+
+udpResponder = threading.Thread(name='UDP Proxy Protocol Responder', target=ProxyProtocolUDPResponder, args=[proxyResponderPort, toProxyQueue, fromProxyQueue])
+udpResponder.setDaemon(True)
+udpResponder.start()
+tcpResponder = threading.Thread(name='TCP Proxy Protocol Responder', target=ProxyProtocolTCPResponder, args=[proxyResponderPort, toProxyQueue, fromProxyQueue])
+tcpResponder.setDaemon(True)
+tcpResponder.start()
+
+class ProxyProtocolTest(DNSDistTest):
+    _proxyResponderPort = proxyResponderPort
+    _config_params = ['_proxyResponderPort']
+
+    def checkMessageProxyProtocol(self, receivedProxyPayload, source, destination, isTCP, values=[]):
+      proxy = ProxyProtocol()
+      self.assertTrue(proxy.parseHeader(receivedProxyPayload))
+      self.assertEquals(proxy.version, 0x02)
+      self.assertEquals(proxy.command, 0x01)
+      self.assertEquals(proxy.family, 0x01)
+      if not isTCP:
+        self.assertEquals(proxy.protocol, 0x02)
+      else:
+        self.assertEquals(proxy.protocol, 0x01)
+      self.assertGreater(proxy.contentLen, 0)
+
+      self.assertTrue(proxy.parseAddressesAndPorts(receivedProxyPayload))
+      self.assertEquals(proxy.source, source)
+      self.assertEquals(proxy.destination, destination)
+      #self.assertEquals(proxy.sourcePort, sourcePort)
+      self.assertEquals(proxy.destinationPort, self._dnsDistPort)
+
+      self.assertTrue(proxy.parseAdditionalValues(receivedProxyPayload))
+      proxy.values.sort()
+      values.sort()
+      self.assertEquals(proxy.values, values)
+
+class TestProxyProtocol(ProxyProtocolTest):
+    """
+    dnsdist is configured to prepend a Proxy Protocol header to the query
+    """
+
+    _config_template = """
+    newServer{address="127.0.0.1:%d", useProxyProtocol=true}
+
+    function addValues(dq)
+      local values = { [0]="foo", [42]="bar" }
+      dq:setProxyProtocolValues(values)
+      return DNSAction.None
+    end
+
+    addAction("values-lua.proxy.tests.powerdns.com.", LuaAction(addValues))
+    addAction("values-action.proxy.tests.powerdns.com.", SetProxyProtocolValuesAction({ ["1"]="dnsdist", ["255"]="proxy-protocol"}))
+    """
+    _config_params = ['_proxyResponderPort']
+
+    def testProxyUDP(self):
+        """
+        Proxy Protocol: no value (UDP)
+        """
+        name = 'simple-udp.proxy.tests.powerdns.com.'
+        query = dns.message.make_query(name, 'A', 'IN')
+        response = dns.message.make_response(query)
+
+        toProxyQueue.put(response, True, 2.0)
+
+        data = query.to_wire()
+        self._sock.send(data)
+        receivedResponse = None
+        try:
+            self._sock.settimeout(2.0)
+            data = self._sock.recv(4096)
+        except socket.timeout:
+            print('timeout')
+            data = None
+        if data:
+            receivedResponse = dns.message.from_wire(data)
+
+        (receivedProxyPayload, receivedDNSData) = fromProxyQueue.get(True, 2.0)
+        self.assertTrue(receivedProxyPayload)
+        self.assertTrue(receivedDNSData)
+        self.assertTrue(receivedResponse)
+
+        receivedQuery = dns.message.from_wire(receivedDNSData)
+        receivedQuery.id = query.id
+        receivedResponse.id = response.id
+        self.assertEquals(receivedQuery, query)
+        self.assertEquals(receivedResponse, response)
+        self.checkMessageProxyProtocol(receivedProxyPayload, '127.0.0.1', '127.0.0.1', False)
+
+    def testProxyTCP(self):
+      """
+        Proxy Protocol: no value (TCP)
+      """
+      name = 'simple-tcp.proxy.tests.powerdns.com.'
+      query = dns.message.make_query(name, 'A', 'IN')
+      response = dns.message.make_response(query)
+
+      toProxyQueue.put(response, True, 2.0)
+
+      conn = self.openTCPConnection(2.0)
+      data = query.to_wire()
+      self.sendTCPQueryOverConnection(conn, data, rawQuery=True)
+      receivedResponse = None
+      try:
+        receivedResponse = self.recvTCPResponseOverConnection(conn)
+      except socket.timeout:
+            print('timeout')
+
+      (receivedProxyPayload, receivedDNSData) = fromProxyQueue.get(True, 2.0)
+      self.assertTrue(receivedProxyPayload)
+      self.assertTrue(receivedDNSData)
+      self.assertTrue(receivedResponse)
+
+      receivedQuery = dns.message.from_wire(receivedDNSData)
+      receivedQuery.id = query.id
+      receivedResponse.id = response.id
+      self.assertEquals(receivedQuery, query)
+      self.assertEquals(receivedResponse, response)
+      self.checkMessageProxyProtocol(receivedProxyPayload, '127.0.0.1', '127.0.0.1', True)
+
+    def testProxyUDPWithValuesFromLua(self):
+        """
+        Proxy Protocol: values from Lua (UDP)
+        """
+        name = 'values-lua.proxy.tests.powerdns.com.'
+        query = dns.message.make_query(name, 'A', 'IN')
+        response = dns.message.make_response(query)
+
+        toProxyQueue.put(response, True, 2.0)
+
+        data = query.to_wire()
+        self._sock.send(data)
+        receivedResponse = None
+        try:
+            self._sock.settimeout(2.0)
+            data = self._sock.recv(4096)
+        except socket.timeout:
+            print('timeout')
+            data = None
+        if data:
+            receivedResponse = dns.message.from_wire(data)
+
+        (receivedProxyPayload, receivedDNSData) = fromProxyQueue.get(True, 2.0)
+        self.assertTrue(receivedProxyPayload)
+        self.assertTrue(receivedDNSData)
+        self.assertTrue(receivedResponse)
+
+        receivedQuery = dns.message.from_wire(receivedDNSData)
+        receivedQuery.id = query.id
+        receivedResponse.id = response.id
+        self.assertEquals(receivedQuery, query)
+        self.assertEquals(receivedResponse, response)
+        self.checkMessageProxyProtocol(receivedProxyPayload, '127.0.0.1', '127.0.0.1', False, [ [0, b'foo'] , [ 42, b'bar'] ])
+
+    def testProxyTCPWithValuesFromLua(self):
+      """
+        Proxy Protocol: values from Lua (TCP)
+      """
+      name = 'values-lua.proxy.tests.powerdns.com.'
+      query = dns.message.make_query(name, 'A', 'IN')
+      response = dns.message.make_response(query)
+
+      toProxyQueue.put(response, True, 2.0)
+
+      conn = self.openTCPConnection(2.0)
+      data = query.to_wire()
+      self.sendTCPQueryOverConnection(conn, data, rawQuery=True)
+      receivedResponse = None
+      try:
+        receivedResponse = self.recvTCPResponseOverConnection(conn)
+      except socket.timeout:
+            print('timeout')
+
+      (receivedProxyPayload, receivedDNSData) = fromProxyQueue.get(True, 2.0)
+      self.assertTrue(receivedProxyPayload)
+      self.assertTrue(receivedDNSData)
+      self.assertTrue(receivedResponse)
+
+      receivedQuery = dns.message.from_wire(receivedDNSData)
+      receivedQuery.id = query.id
+      receivedResponse.id = response.id
+      self.assertEquals(receivedQuery, query)
+      self.assertEquals(receivedResponse, response)
+      self.checkMessageProxyProtocol(receivedProxyPayload, '127.0.0.1', '127.0.0.1', True, [ [0, b'foo'] , [ 42, b'bar'] ])
+
+    def testProxyUDPWithValuesFromAction(self):
+        """
+        Proxy Protocol: values from Action (UDP)
+        """
+        name = 'values-action.proxy.tests.powerdns.com.'
+        query = dns.message.make_query(name, 'A', 'IN')
+        response = dns.message.make_response(query)
+
+        toProxyQueue.put(response, True, 2.0)
+
+        data = query.to_wire()
+        self._sock.send(data)
+        receivedResponse = None
+        try:
+            self._sock.settimeout(2.0)
+            data = self._sock.recv(4096)
+        except socket.timeout:
+            print('timeout')
+            data = None
+        if data:
+            receivedResponse = dns.message.from_wire(data)
+
+        (receivedProxyPayload, receivedDNSData) = fromProxyQueue.get(True, 2.0)
+        self.assertTrue(receivedProxyPayload)
+        self.assertTrue(receivedDNSData)
+        self.assertTrue(receivedResponse)
+
+        receivedQuery = dns.message.from_wire(receivedDNSData)
+        receivedQuery.id = query.id
+        receivedResponse.id = response.id
+        self.assertEquals(receivedQuery, query)
+        self.assertEquals(receivedResponse, response)
+        self.checkMessageProxyProtocol(receivedProxyPayload, '127.0.0.1', '127.0.0.1', False, [ [1, b'dnsdist'] , [ 255, b'proxy-protocol'] ])
+
+    def testProxyTCPWithValuesFromAction(self):
+      """
+        Proxy Protocol: values from Action (TCP)
+      """
+      name = 'values-action.proxy.tests.powerdns.com.'
+      query = dns.message.make_query(name, 'A', 'IN')
+      response = dns.message.make_response(query)
+
+      toProxyQueue.put(response, True, 2.0)
+
+      conn = self.openTCPConnection(2.0)
+      data = query.to_wire()
+      self.sendTCPQueryOverConnection(conn, data, rawQuery=True)
+      receivedResponse = None
+      try:
+        receivedResponse = self.recvTCPResponseOverConnection(conn)
+      except socket.timeout:
+            print('timeout')
+
+      (receivedProxyPayload, receivedDNSData) = fromProxyQueue.get(True, 2.0)
+      self.assertTrue(receivedProxyPayload)
+      self.assertTrue(receivedDNSData)
+      self.assertTrue(receivedResponse)
+
+      receivedQuery = dns.message.from_wire(receivedDNSData)
+      receivedQuery.id = query.id
+      receivedResponse.id = response.id
+      self.assertEquals(receivedQuery, query)
+      self.assertEquals(receivedResponse, response)
+      self.checkMessageProxyProtocol(receivedProxyPayload, '127.0.0.1', '127.0.0.1', True, [ [1, b'dnsdist'] , [ 255, b'proxy-protocol'] ])
+
+    def testProxyTCPSeveralQueriesOnSameConnection(self):
+      """
+        Proxy Protocol: Several queries on the same TCP connection
+      """
+      name = 'several-queries-same-conn.proxy.tests.powerdns.com.'
+      query = dns.message.make_query(name, 'A', 'IN')
+      response = dns.message.make_response(query)
+
+      conn = self.openTCPConnection(2.0)
+      data = query.to_wire()
+
+      for idx in range(10):
+        toProxyQueue.put(response, True, 2.0)
+        self.sendTCPQueryOverConnection(conn, data, rawQuery=True)
+        receivedResponse = None
+        try:
+          receivedResponse = self.recvTCPResponseOverConnection(conn)
+        except socket.timeout:
+          print('timeout')
+
+        (receivedProxyPayload, receivedDNSData) = fromProxyQueue.get(True, 2.0)
+        self.assertTrue(receivedProxyPayload)
+        self.assertTrue(receivedDNSData)
+        self.assertTrue(receivedResponse)
+
+        receivedQuery = dns.message.from_wire(receivedDNSData)
+        receivedQuery.id = query.id
+        receivedResponse.id = response.id
+        self.assertEquals(receivedQuery, query)
+        self.assertEquals(receivedResponse, response)
+        self.checkMessageProxyProtocol(receivedProxyPayload, '127.0.0.1', '127.0.0.1', True, [])
diff --git a/regression-tests.recursor-dnssec/proxyprotocol.py b/regression-tests.recursor-dnssec/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
index a6f688fc3d37487213ca077ec38f0f9bfb941ea1..2770e944776f1626046dfa702962185c3260c044 100644 (file)
@@ -40,6 +40,7 @@ max-cache-ttl=15
 threads=1
 loglevel=9
 disable-syslog=yes
+log-common-errors=yes
 """
     _config_template = """
 """
diff --git a/regression-tests.recursor-dnssec/test_ProxyProtocol.py b/regression-tests.recursor-dnssec/test_ProxyProtocol.py
new file mode 100644 (file)
index 0000000..fcc489b
--- /dev/null
@@ -0,0 +1,661 @@
+import dns
+import os
+import socket
+import struct
+import sys
+import time
+
+try:
+    range = xrange
+except NameError:
+    pass
+
+from recursortests import RecursorTest
+from proxyprotocol import ProxyProtocol
+
+class ProxyProtocolRecursorTest(RecursorTest):
+
+    @classmethod
+    def setUpClass(cls):
+
+        # we don't need all the auth stuff
+        cls.setUpSockets()
+        cls.startResponders()
+
+        confdir = os.path.join('configs', cls._confdir)
+        cls.createConfigDir(confdir)
+
+        cls.generateRecursorConfig(confdir)
+        cls.startRecursor(confdir, cls._recursorPort)
+
+    @classmethod
+    def tearDownClass(cls):
+        cls.tearDownRecursor()
+
+    @classmethod
+    def sendUDPQueryWithProxyProtocol(cls, query, v6, source, destination, sourcePort, destinationPort, values=[], timeout=2.0):
+        queryPayload = query.to_wire()
+        ppPayload = ProxyProtocol.getPayload(False, False, v6, source, destination, sourcePort, destinationPort, values)
+        payload = ppPayload + queryPayload
+
+        if timeout:
+            cls._sock.settimeout(timeout)
+
+        try:
+            cls._sock.send(payload)
+            data = cls._sock.recv(4096)
+        except socket.timeout:
+            data = None
+        finally:
+            if timeout:
+                cls._sock.settimeout(None)
+
+        message = None
+        if data:
+            message = dns.message.from_wire(data)
+        return message
+
+    @classmethod
+    def sendTCPQueryWithProxyProtocol(cls, query, v6, source, destination, sourcePort, destinationPort, values=[], timeout=2.0):
+        queryPayload = query.to_wire()
+        ppPayload = ProxyProtocol.getPayload(False, False, v6, source, destination, sourcePort, destinationPort, values)
+
+        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        if timeout:
+            sock.settimeout(timeout)
+
+        sock.connect(("127.0.0.1", cls._recursorPort))
+
+        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()
+
+        message = None
+        if data:
+            message = dns.message.from_wire(data)
+        return message
+
+class ProxyProtocolAllowedRecursorTest(ProxyProtocolRecursorTest):
+    _confdir = 'ProxyProtocol'
+    _lua_dns_script_file = """
+
+    function gettag(remote, ednssubnet, localip, qname, qtype, ednsoptions, tcp, proxyProtocolValues)
+      local remoteaddr = remote:toStringWithPort()
+      local localaddr = localip:toStringWithPort()
+      local foundFoo = false
+      local foundBar = false
+
+      if remoteaddr ~= '127.0.0.42:0' and remoteaddr ~= '[::42]:0' then
+        pdnslog('gettag: invalid source '..remoteaddr)
+        return 1
+      end
+      if localaddr ~= '255.255.255.255:65535' and localaddr ~= '[2001:db8::ff]:65535' then
+        pdnslog('gettag: invalid dest '..localaddr)
+        return 2
+      end
+
+      for k,v in pairs(proxyProtocolValues) do
+        local type = v:getType()
+        local content = v:getContent()
+        if type == 0 and content == 'foo' then
+          foundFoo = true
+        end
+        if type == 255 and content == 'bar' then
+          foundBar = true
+        end
+      end
+
+      if not foundFoo or not foundBar then
+        pdnslog('gettag: TLV not found')
+        return 3
+      end
+
+      return 42
+    end
+
+    function preresolve(dq)
+      local foundFoo = false
+      local foundBar = false
+      local values = dq:getProxyProtocolValues()
+      for k,v in pairs(values) do
+        local type = v:getType()
+        local content = v:getContent()
+        if type == 0 and content == 'foo' then
+          foundFoo = true
+        end
+        if type == 255 and content == 'bar' then
+          foundBar = true
+        end
+      end
+
+      if not foundFoo or not foundBar then
+        pdnslog('TLV not found')
+        dq:addAnswer(pdns.A, '192.0.2.255', 60)
+        return true
+      end
+
+      local remoteaddr = dq.remoteaddr:toStringWithPort()
+      local localaddr = dq.localaddr:toStringWithPort()
+
+      if remoteaddr ~= '127.0.0.42:0' and remoteaddr ~= '[::42]:0' then
+        pdnslog('invalid source '..remoteaddr)
+        dq:addAnswer(pdns.A, '192.0.2.128', 60)
+        return true
+      end
+      if localaddr ~= '255.255.255.255:65535' and localaddr ~= '[2001:db8::ff]:65535' then
+        pdnslog('invalid dest '..localaddr)
+        dq:addAnswer(pdns.A, '192.0.2.129', 60)
+        return true
+      end
+
+      if dq.tag ~= 42 then
+        pdnslog('invalid tag '..dq.tag)
+        dq:addAnswer(pdns.A, '192.0.2.130', 60)
+        return true
+      end
+
+      dq:addAnswer(pdns.A, '192.0.2.1', 60)
+      return true
+    end
+    """
+
+    _config_template = """
+    proxy-protocol-from=127.0.0.1
+    proxy-protocol-maximum-size=512
+    allow-from=127.0.0.0/24, ::1/128, ::42/128
+""" % ()
+
+    def testLocalProxyProtocol(self):
+        qname = 'local.proxy-protocol.recursor-tests.powerdns.com.'
+        expected = dns.rrset.from_text(qname, 0, dns.rdataclass.IN, 'A', '192.0.2.255')
+
+        query = dns.message.make_query(qname, 'A', want_dnssec=True)
+        queryPayload = query.to_wire()
+        ppPayload = ProxyProtocol.getPayload(True, False, False, None, None, None, None, [])
+        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)
+        self.assertRcodeEqual(res, dns.rcode.NOERROR)
+        self.assertRRsetInAnswer(res, expected)
+
+        # TCP
+        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        sock.settimeout(2.0)
+        sock.connect(("127.0.0.1", self._recursorPort))
+
+        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.assertRRsetInAnswer(res, expected)
+
+    def testInvalidMagicProxyProtocol(self):
+        qname = 'invalid-magic.proxy-protocol.recursor-tests.powerdns.com.'
+
+        query = dns.message.make_query(qname, 'A', want_dnssec=True)
+        queryPayload = query.to_wire()
+        ppPayload = ProxyProtocol.getPayload(True, False, False, None, None, None, None, [])
+        ppPayload = b'\x00' + ppPayload[1:]
+        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)
+        self.assertEqual(res, None)
+
+        # TCP
+        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        sock.settimeout(2.0)
+        sock.connect(("127.0.0.1", self._recursorPort))
+
+        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.assertEqual(res, None)
+
+    def testTCPOneByteAtATimeProxyProtocol(self):
+        qname = 'tcp-one-byte-at-a-time.proxy-protocol.recursor-tests.powerdns.com.'
+        expected = dns.rrset.from_text(qname, 0, dns.rdataclass.IN, 'A', '192.0.2.1')
+
+        query = dns.message.make_query(qname, 'A', want_dnssec=True)
+        queryPayload = query.to_wire()
+        ppPayload = ProxyProtocol.getPayload(False, True, False, '127.0.0.42', '255.255.255.255', 0, 65535, [ [0, b'foo' ], [ 255, b'bar'] ])
+
+        # TCP
+        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        sock.settimeout(2.0)
+        sock.connect(("127.0.0.1", self._recursorPort))
+
+        try:
+            for i in range(len(ppPayload)):
+                sock.send(ppPayload[i:i+1])
+                time.sleep(0.01)
+            value = struct.pack("!H", len(queryPayload))
+            for i in range(len(value)):
+                sock.send(value[i:i+1])
+                time.sleep(0.01)
+            for i in range(len(queryPayload)):
+                sock.send(queryPayload[i:i+1])
+                time.sleep(0.01)
+
+            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.assertRRsetInAnswer(res, expected)
+
+    def testTooLargeProxyProtocol(self):
+        # the total payload (proxy protocol + DNS) is larger than proxy-protocol-maximum-size
+        # so it should be dropped
+        qname = 'too-large.proxy-protocol.recursor-tests.powerdns.com.'
+        expected = dns.rrset.from_text(qname, 0, dns.rdataclass.IN, 'A', '192.0.2.1')
+
+        query = dns.message.make_query(qname, 'A', want_dnssec=True)
+        queryPayload = query.to_wire()
+        ppPayload = ProxyProtocol.getPayload(False, True, False, '127.0.0.42', '255.255.255.255', 0, 65535, [ [0, b'foo' ], [1, b'A'*512], [ 255, b'bar'] ])
+        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)
+        self.assertEqual(res, None)
+
+        # TCP
+        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        sock.settimeout(2.0)
+        sock.connect(("127.0.0.1", self._recursorPort))
+
+        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.assertEqual(res, None)
+
+    def testNoHeaderProxyProtocol(self):
+        qname = 'no-header.proxy-protocol.recursor-tests.powerdns.com.'
+
+        query = dns.message.make_query(qname, 'A', want_dnssec=True)
+        for method in ("sendUDPQuery", "sendTCPQuery"):
+            sender = getattr(self, method)
+            res = sender(query)
+            self.assertEqual(res, None)
+
+    def testIPv4ProxyProtocol(self):
+        qname = 'ipv4.proxy-protocol.recursor-tests.powerdns.com.'
+        expected = dns.rrset.from_text(qname, 0, dns.rdataclass.IN, 'A', '192.0.2.1')
+
+        query = dns.message.make_query(qname, 'A', want_dnssec=True)
+        for method in ("sendUDPQueryWithProxyProtocol", "sendTCPQueryWithProxyProtocol"):
+            sender = getattr(self, method)
+            res = sender(query, False, '127.0.0.42', '255.255.255.255', 0, 65535, [ [0, b'foo' ], [ 255, b'bar'] ])
+            self.assertRcodeEqual(res, dns.rcode.NOERROR)
+            self.assertRRsetInAnswer(res, expected)
+
+    def testIPv4NoValuesProxyProtocol(self):
+        qname = 'ipv4-no-values.proxy-protocol.recursor-tests.powerdns.com.'
+        expected = dns.rrset.from_text(qname, 0, dns.rdataclass.IN, 'A', '192.0.2.255')
+
+        query = dns.message.make_query(qname, 'A', want_dnssec=True)
+        for method in ("sendUDPQueryWithProxyProtocol", "sendTCPQueryWithProxyProtocol"):
+            sender = getattr(self, method)
+            res = sender(query, False, '127.0.0.42', '255.255.255.255', 0, 65535)
+            self.assertRcodeEqual(res, dns.rcode.NOERROR)
+            self.assertRRsetInAnswer(res, expected)
+
+    def testIPv4ProxyProtocolNotAuthorized(self):
+        qname = 'ipv4-not-authorized.proxy-protocol.recursor-tests.powerdns.com.'
+
+        query = dns.message.make_query(qname, 'A', want_dnssec=True)
+        for method in ("sendUDPQueryWithProxyProtocol", "sendTCPQueryWithProxyProtocol"):
+            sender = getattr(self, method)
+            res = sender(query, False, '192.0.2.255', '255.255.255.255', 0, 65535, [ [0, b'foo' ], [ 255, b'bar'] ])
+            self.assertEqual(res, None)
+
+    def testIPv6ProxyProtocol(self):
+        qname = 'ipv6.proxy-protocol.recursor-tests.powerdns.com.'
+        expected = dns.rrset.from_text(qname, 0, dns.rdataclass.IN, 'A', '192.0.2.1')
+
+        query = dns.message.make_query(qname, 'A', want_dnssec=True)
+        for method in ("sendUDPQueryWithProxyProtocol", "sendTCPQueryWithProxyProtocol"):
+            sender = getattr(self, method)
+            res = sender(query, True, '::42', '2001:db8::ff', 0, 65535, [ [0, b'foo' ], [ 255, b'bar'] ])
+            self.assertRcodeEqual(res, dns.rcode.NOERROR)
+            self.assertRRsetInAnswer(res, expected)
+
+    def testIPv6NoValuesProxyProtocol(self):
+        qname = 'ipv6-no-values.proxy-protocol.recursor-tests.powerdns.com.'
+        expected = dns.rrset.from_text(qname, 0, dns.rdataclass.IN, 'A', '192.0.2.255')
+
+        query = dns.message.make_query(qname, 'A', want_dnssec=True)
+        for method in ("sendUDPQueryWithProxyProtocol", "sendTCPQueryWithProxyProtocol"):
+            sender = getattr(self, method)
+            res = sender(query, True, '::42', '2001:db8::ff', 0, 65535)
+            self.assertRcodeEqual(res, dns.rcode.NOERROR)
+            self.assertRRsetInAnswer(res, expected)
+
+    def testIPv6ProxyProtocolNotAuthorized(self):
+        qname = 'ipv6-not-authorized.proxy-protocol.recursor-tests.powerdns.com.'
+
+        query = dns.message.make_query(qname, 'A', want_dnssec=True)
+        for method in ("sendUDPQueryWithProxyProtocol", "sendTCPQueryWithProxyProtocol"):
+            sender = getattr(self, method)
+            res = sender(query, True, '2001:db8::1', '2001:db8::ff', 0, 65535, [ [0, b'foo' ], [ 255, b'bar'] ])
+            self.assertEqual(res, None)
+
+    def testIPv6ProxyProtocolSeveralQueriesOverTCP(self):
+        qname = 'several-queries-tcp.proxy-protocol.recursor-tests.powerdns.com.'
+        expected = dns.rrset.from_text(qname, 0, dns.rdataclass.IN, 'A', '192.0.2.1')
+
+        query = dns.message.make_query(qname, 'A', want_dnssec=True)
+        queryPayload = query.to_wire()
+        ppPayload = ProxyProtocol.getPayload(False, True, True, '::42', '2001:db8::ff', 0, 65535, [ [0, b'foo' ], [ 255, b'bar'] ])
+        payload = ppPayload + queryPayload
+
+        # TCP
+        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        sock.settimeout(2.0)
+        sock.connect(("127.0.0.1", self._recursorPort))
+
+        sock.send(ppPayload)
+
+        count = 0
+        for idx in range(5):
+            try:
+                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
+                break
+            except socket.error as e:
+                print("Network error: %s" % (str(e)))
+                data = None
+                break
+
+            res = None
+            if data:
+                res = dns.message.from_wire(data)
+            self.assertRcodeEqual(res, dns.rcode.NOERROR)
+            self.assertRRsetInAnswer(res, expected)
+            count = count + 1
+
+        self.assertEqual(count, 5)
+        sock.close()
+
+class ProxyProtocolAllowedFFIRecursorTest(ProxyProtocolAllowedRecursorTest):
+    # same tests than ProxyProtocolAllowedRecursorTest but with the Lua FFI interface instead of the regular one
+    _confdir = 'ProxyProtocolFFI'
+    _lua_dns_script_file = """
+    local ffi = require("ffi")
+
+    ffi.cdef[[
+      typedef struct pdns_ffi_param pdns_ffi_param_t;
+
+      typedef struct pdns_proxyprotocol_value {
+        uint8_t     type;
+        uint16_t    len;
+        const void* data;
+      } pdns_proxyprotocol_value_t;
+
+      size_t pdns_ffi_param_get_proxy_protocol_values(pdns_ffi_param_t* ref, const pdns_proxyprotocol_value_t** out);
+      const char* pdns_ffi_param_get_remote(pdns_ffi_param_t* ref);
+      const char* pdns_ffi_param_get_local(pdns_ffi_param_t* ref);
+      uint16_t pdns_ffi_param_get_remote_port(const pdns_ffi_param_t* ref);
+      uint16_t pdns_ffi_param_get_local_port(const pdns_ffi_param_t* ref);
+
+      void pdns_ffi_param_set_tag(pdns_ffi_param_t* ref, unsigned int tag);
+    ]]
+
+    function gettag_ffi(obj)
+      local remoteaddr = ffi.string(ffi.C.pdns_ffi_param_get_remote(obj))
+      local localaddr = ffi.string(ffi.C.pdns_ffi_param_get_local(obj))
+      local foundFoo = false
+      local foundBar = false
+
+      if remoteaddr ~= '127.0.0.42' and remoteaddr ~= '::42' then
+        pdnslog('gettag-ffi: invalid source '..remoteaddr)
+        ffi.C.pdns_ffi_param_set_tag(obj, 1)
+        return
+      end
+      if localaddr ~= '255.255.255.255' and localaddr ~= '2001:db8::ff' then
+        pdnslog('gettag-ffi: invalid dest '..localaddr)
+        ffi.C.pdns_ffi_param_set_tag(obj, 2)
+        return
+      end
+
+      if ffi.C.pdns_ffi_param_get_remote_port(obj) ~= 0 then
+        pdnslog('gettag-ffi: invalid source port '..ffi.C.pdns_ffi_param_get_remote_port(obj))
+        ffi.C.pdns_ffi_param_set_tag(obj, 1)
+        return
+      end
+
+      if ffi.C.pdns_ffi_param_get_local_port(obj) ~= 65535 then
+        pdnslog('gettag-ffi: invalid source port '..ffi.C.pdns_ffi_param_get_local_port(obj))
+        ffi.C.pdns_ffi_param_set_tag(obj, 2)
+        return
+      end
+
+      local ret_ptr = ffi.new("const pdns_proxyprotocol_value_t *[1]")
+      local ret_ptr_param = ffi.cast("const pdns_proxyprotocol_value_t **", ret_ptr)
+      local values_count = ffi.C.pdns_ffi_param_get_proxy_protocol_values(obj, ret_ptr_param)
+
+      if values_count > 0 then
+        for i = 0,tonumber(values_count)-1 do
+          local type = ret_ptr[0][i].type
+          local content = ffi.string(ret_ptr[0][i].data, ret_ptr[0][i].len)
+          if type == 0 and content == 'foo' then
+            foundFoo = true
+          end
+          if type == 255 and content == 'bar' then
+            foundBar = true
+          end
+        end
+      end
+
+      if not foundFoo or not foundBar then
+        pdnslog('gettag-ffi: TLV not found')
+        ffi.C.pdns_ffi_param_set_tag(obj, 3)
+        return
+      end
+
+      ffi.C.pdns_ffi_param_set_tag(obj, 42)
+    end
+
+    function preresolve(dq)
+      local foundFoo = false
+      local foundBar = false
+      local values = dq:getProxyProtocolValues()
+      for k,v in pairs(values) do
+        local type = v:getType()
+        local content = v:getContent()
+        if type == 0 and content == 'foo' then
+          foundFoo = true
+        end
+        if type == 255 and content == 'bar' then
+          foundBar = true
+        end
+      end
+
+      if not foundFoo or not foundBar then
+        pdnslog('TLV not found')
+        dq:addAnswer(pdns.A, '192.0.2.255', 60)
+        return true
+      end
+
+      local remoteaddr = dq.remoteaddr:toStringWithPort()
+      local localaddr = dq.localaddr:toStringWithPort()
+
+      if remoteaddr ~= '127.0.0.42:0' and remoteaddr ~= '[::42]:0' then
+        pdnslog('invalid source '..remoteaddr)
+        dq:addAnswer(pdns.A, '192.0.2.128', 60)
+        return true
+      end
+      if localaddr ~= '255.255.255.255:65535' and localaddr ~= '[2001:db8::ff]:65535' then
+        pdnslog('invalid dest '..localaddr)
+        dq:addAnswer(pdns.A, '192.0.2.129', 60)
+        return true
+      end
+
+      if dq.tag ~= 42 then
+        pdnslog('invalid tag '..dq.tag)
+        dq:addAnswer(pdns.A, '192.0.2.130', 60)
+        return true
+      end
+
+      dq:addAnswer(pdns.A, '192.0.2.1', 60)
+      return true
+    end
+    """
+
+class ProxyProtocolNotAllowedRecursorTest(ProxyProtocolRecursorTest):
+    _confdir = 'ProxyProtocolNotAllowed'
+    _lua_dns_script_file = """
+
+    function preresolve(dq)
+      dq:addAnswer(pdns.A, '192.0.2.1', 60)
+      return true
+    end
+    """
+
+    _config_template = """
+    proxy-protocol-from=192.0.2.1/32
+    allow-from=127.0.0.0/24, ::1/128
+""" % ()
+
+    def testNoHeaderProxyProtocol(self):
+        qname = 'no-header.proxy-protocol-not-allowed.recursor-tests.powerdns.com.'
+        expected = dns.rrset.from_text(qname, 0, dns.rdataclass.IN, 'A', '192.0.2.1')
+
+        query = dns.message.make_query(qname, 'A', want_dnssec=True)
+        for method in ("sendUDPQuery", "sendTCPQuery"):
+            sender = getattr(self, method)
+            res = sender(query)
+            self.assertRcodeEqual(res, dns.rcode.NOERROR)
+            self.assertRRsetInAnswer(res, expected)
+
+    def testIPv4ProxyProtocol(self):
+        qname = 'ipv4.proxy-protocol-not-allowed.recursor-tests.powerdns.com.'
+        expected = dns.rrset.from_text(qname, 0, dns.rdataclass.IN, 'A', '192.0.2.1')
+
+        query = dns.message.make_query(qname, 'A', want_dnssec=True)
+        for method in ("sendUDPQueryWithProxyProtocol", "sendTCPQueryWithProxyProtocol"):
+            sender = getattr(self, method)
+            res = sender(query, False, '127.0.0.42', '255.255.255.255', 0, 65535, [ [0, b'foo' ], [ 255, b'bar'] ])
+            self.assertEqual(res, None)