]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
dnsdist: Use an eBPF filter for Dynamic blocks when available
authorRemi Gacogne <remi.gacogne@powerdns.com>
Fri, 27 Nov 2020 10:18:27 +0000 (11:18 +0100)
committerRemi Gacogne <remi.gacogne@powerdns.com>
Thu, 3 Dec 2020 08:42:24 +0000 (09:42 +0100)
12 files changed:
pdns/bpf-filter.cc
pdns/bpf-filter.hh
pdns/dnsdist-dynbpf.cc
pdns/dnsdist-dynbpf.hh
pdns/dnsdist-lua-bindings.cc
pdns/dnsdist-lua.cc
pdns/dnsdist.cc
pdns/dnsdist.hh
pdns/dnsdistdist/dnsdist-dynblocks.cc
pdns/dnsdistdist/docs/advanced/ebpf.rst
pdns/dnsdistdist/docs/guides/dynblocks.rst
pdns/dnsdistdist/test-dnsdistdynblocks_hh.cc

index 96b7acdc5e71f9aca20dbf96efe8581d223990d6..7e833f42fa89e31bcd4d4e4e720d01b28d359861 100644 (file)
@@ -214,16 +214,15 @@ void BPFFilter::removeSocket(int sock)
 
 void BPFFilter::block(const ComboAddress& addr)
 {
-  std::lock_guard<std::mutex> lock(d_mutex);
-
   uint64_t counter = 0;
   int res = 0;
-  if (addr.sin4.sin_family == AF_INET) {
+  if (addr.isIPv4()) {
     uint32_t key = htonl(addr.sin4.sin_addr.s_addr);
     if (d_v4Count >= d_maxV4) {
       throw std::runtime_error("Table full when trying to block " + addr.toString());
     }
 
+    std::lock_guard<std::mutex> lock(d_mutex);
     res = bpf_lookup_elem(d_v4map.fd, &key, &counter);
     if (res != -1) {
       throw std::runtime_error("Trying to block an already blocked address: " + addr.toString());
@@ -234,7 +233,7 @@ void BPFFilter::block(const ComboAddress& addr)
       d_v4Count++;
     }
   }
-  else if (addr.sin4.sin_family == AF_INET6) {
+  else if (addr.isIPv6()) {
     uint8_t key[16];
     static_assert(sizeof(addr.sin6.sin6_addr.s6_addr) == sizeof(key), "POSIX mandates s6_addr to be an array of 16 uint8_t");
     for (size_t idx = 0; idx < sizeof(key); idx++) {
@@ -245,6 +244,7 @@ void BPFFilter::block(const ComboAddress& addr)
       throw std::runtime_error("Table full when trying to block " + addr.toString());
     }
 
+    std::lock_guard<std::mutex> lock(d_mutex);
     res = bpf_lookup_elem(d_v6map.fd, &key, &counter);
     if (res != -1) {
       throw std::runtime_error("Trying to block an already blocked address: " + addr.toString());
@@ -263,23 +263,23 @@ void BPFFilter::block(const ComboAddress& addr)
 
 void BPFFilter::unblock(const ComboAddress& addr)
 {
-  std::lock_guard<std::mutex> lock(d_mutex);
-
   int res = 0;
-  if (addr.sin4.sin_family == AF_INET) {
+  if (addr.isIPv4()) {
     uint32_t key = htonl(addr.sin4.sin_addr.s_addr);
+    std::lock_guard<std::mutex> lock(d_mutex);
     res = bpf_delete_elem(d_v4map.fd, &key);
     if (res == 0) {
       d_v4Count--;
     }
   }
-  else if (addr.sin4.sin_family == AF_INET6) {
+  else if (addr.isIPv6()) {
     uint8_t key[16];
     static_assert(sizeof(addr.sin6.sin6_addr.s6_addr) == sizeof(key), "POSIX mandates s6_addr to be an array of 16 uint8_t");
     for (size_t idx = 0; idx < sizeof(key); idx++) {
       key[idx] = addr.sin6.sin6_addr.s6_addr[idx];
     }
 
+    std::lock_guard<std::mutex> lock(d_mutex);
     res = bpf_delete_elem(d_v6map.fd, key);
     if (res == 0) {
       d_v6Count--;
@@ -355,25 +355,15 @@ void BPFFilter::unblock(const DNSName& qname, uint16_t qtype)
 std::vector<std::pair<ComboAddress, uint64_t> > BPFFilter::getAddrStats()
 {
   std::vector<std::pair<ComboAddress, uint64_t> > result;
-  std::lock_guard<std::mutex> lock(d_mutex);
+  result.reserve(d_v4Count + d_v6Count);
 
-  uint32_t v4Key = 0;
-  uint32_t nextV4Key;
-  uint64_t value;
-  int res = bpf_get_next_key(d_v4map.fd, &v4Key, &nextV4Key);
   sockaddr_in v4Addr;
   memset(&v4Addr, 0, sizeof(v4Addr));
   v4Addr.sin_family = AF_INET;
 
-  while (res == 0) {
-    v4Key = nextV4Key;
-    if (bpf_lookup_elem(d_v4map.fd, &v4Key, &value) == 0) {
-      v4Addr.sin_addr.s_addr = ntohl(v4Key);
-      result.push_back(make_pair(ComboAddress(&v4Addr), value));
-    }
-
-    res = bpf_get_next_key(d_v4map.fd, &v4Key, &nextV4Key);
-  }
+  uint32_t v4Key = 0;
+  uint32_t nextV4Key;
+  uint64_t value;
 
   uint8_t v6Key[16];
   uint8_t nextV6Key[16];
@@ -386,6 +376,19 @@ std::vector<std::pair<ComboAddress, uint64_t> > BPFFilter::getAddrStats()
     v6Key[idx] = 0;
   }
 
+  std::lock_guard<std::mutex> lock(d_mutex);
+  int res = bpf_get_next_key(d_v4map.fd, &v4Key, &nextV4Key);
+
+  while (res == 0) {
+    v4Key = nextV4Key;
+    if (bpf_lookup_elem(d_v4map.fd, &v4Key, &value) == 0) {
+      v4Addr.sin_addr.s_addr = ntohl(v4Key);
+      result.push_back(make_pair(ComboAddress(&v4Addr), value));
+    }
+
+    res = bpf_get_next_key(d_v4map.fd, &v4Key, &nextV4Key);
+  }
+
   res = bpf_get_next_key(d_v6map.fd, &v6Key, &nextV6Key);
 
   while (res == 0) {
@@ -404,12 +407,14 @@ std::vector<std::pair<ComboAddress, uint64_t> > BPFFilter::getAddrStats()
 std::vector<std::tuple<DNSName, uint16_t, uint64_t> > BPFFilter::getQNameStats()
 {
   std::vector<std::tuple<DNSName, uint16_t, uint64_t> > result;
-  std::lock_guard<std::mutex> lock(d_mutex);
+  result.reserve(d_qNamesCount);
 
   struct QNameKey key = { { 0 } };
   struct QNameKey nextKey = { { 0 } };
   struct QNameValue value;
 
+  std::lock_guard<std::mutex> lock(d_mutex);
+
   int res = bpf_get_next_key(d_qnamemap.fd, &key, &nextKey);
 
   while (res == 0) {
@@ -422,4 +427,95 @@ std::vector<std::tuple<DNSName, uint16_t, uint64_t> > BPFFilter::getQNameStats()
   }
   return result;
 }
+
+uint64_t BPFFilter::getHits(const ComboAddress& requestor)
+{
+  uint64_t counter = 0;
+  if (requestor.isIPv4()) {
+    uint32_t key = htonl(requestor.sin4.sin_addr.s_addr);
+
+    std::lock_guard<std::mutex> lock(d_mutex);
+    int res = bpf_lookup_elem(d_v4map.fd, &key, &counter);
+    if (res == 0) {
+      return counter;
+    }
+  }
+  else if (requestor.isIPv6()) {
+    uint8_t key[16];
+    static_assert(sizeof(requestor.sin6.sin6_addr.s6_addr) == sizeof(key), "POSIX mandates s6_addr to be an array of 16 uint8_t");
+    for (size_t idx = 0; idx < sizeof(key); idx++) {
+      key[idx] = requestor.sin6.sin6_addr.s6_addr[idx];
+    }
+
+    std::lock_guard<std::mutex> lock(d_mutex);
+    int res = bpf_lookup_elem(d_v6map.fd, &key, &counter);
+    if (res == 0) {
+      return counter;
+    }
+  }
+
+  return 0;
+}
+
+#else
+
+BPFFilter::BPFFilter(uint32_t maxV4Addresses, uint32_t maxV6Addresses, uint32_t maxQNames): d_maxV4(maxV4Addresses), d_maxV6(maxV6Addresses), d_maxQNames(maxQNames)
+{
+}
+
+void BPFFilter::addSocket(int sock)
+{
+  (void) sock;
+  throw std::runtime_error("eBPF support not enabled");
+}
+
+void BPFFilter::removeSocket(int sock)
+{
+  (void) sock;
+  throw std::runtime_error("eBPF support not enabled");
+}
+
+void BPFFilter::block(const ComboAddress& addr)
+{
+  (void) addr;
+  throw std::runtime_error("eBPF support not enabled");
+}
+
+void BPFFilter::unblock(const ComboAddress& addr)
+{
+  (void) addr;
+  throw std::runtime_error("eBPF support not enabled");
+}
+
+void BPFFilter::block(const DNSName& qname, uint16_t qtype)
+{
+  (void) qname;
+  (void) qtype;
+  throw std::runtime_error("eBPF support not enabled");
+}
+
+void BPFFilter::unblock(const DNSName& qname, uint16_t qtype)
+{
+  (void) qname;
+  (void) qtype;
+  throw std::runtime_error("eBPF support not enabled");
+}
+
+std::vector<std::pair<ComboAddress, uint64_t> > BPFFilter::getAddrStats()
+{
+  std::vector<std::pair<ComboAddress, uint64_t> > result;
+  return result;
+}
+
+std::vector<std::tuple<DNSName, uint16_t, uint64_t> > BPFFilter::getQNameStats()
+{
+  std::vector<std::tuple<DNSName, uint16_t, uint64_t> > result;
+  return result;
+}
+
+uint64_t BPFFilter::getHits(const ComboAddress& requestor)
+{
+  (void) requestor;
+  return 0;
+}
 #endif /* HAVE_EBPF */
index f01d329d56ddefcd690fc36559eb8152b8a6db69..e14a517dccd5a674d58c9f0c24658b0e3544bcd2 100644 (file)
@@ -26,8 +26,6 @@
 
 #include "iputils.hh"
 
-#ifdef HAVE_EBPF
-
 class BPFFilter
 {
 public:
@@ -40,6 +38,8 @@ public:
   void unblock(const DNSName& qname, uint16_t qtype=255);
   std::vector<std::pair<ComboAddress, uint64_t> > getAddrStats();
   std::vector<std::tuple<DNSName, uint16_t, uint64_t> > getQNameStats();
+  uint64_t getHits(const ComboAddress& requestor);
+
 private:
   struct FDWrapper
   {
@@ -65,5 +65,3 @@ private:
   FDWrapper d_mainfilter;
   FDWrapper d_qnamefilter;
 };
-
-#endif /* HAVE_EBPF */
index 74f78f12ed25ef5870fba9391ff023415308e422..e4c2c0d90be469bfc4f78d5ab4bd7568dd0d950b 100644 (file)
@@ -21,8 +21,6 @@
  */
 #include "dnsdist-dynbpf.hh"
 
-#ifdef HAVE_EBPF
-
 bool DynBPFFilter::block(const ComboAddress& addr, const struct timespec& until)
 {
   bool inserted = false;
@@ -84,4 +82,3 @@ std::vector<std::tuple<ComboAddress, uint64_t, struct timespec> > DynBPFFilter::
   return result;
 }
 
-#endif /* HAVE_EBPF */
index cd69c5d13eae832c8f0af14452d89b7066204196..5cfa2e028eaa86f5ba5dcb242cc4ba392fe6493d 100644 (file)
@@ -27,8 +27,6 @@
 #include "bpf-filter.hh"
 #include "iputils.hh"
 
-#ifdef HAVE_EBPF
-
 #include <boost/multi_index_container.hpp>
 #include <boost/multi_index/ordered_index.hpp>
 
@@ -76,4 +74,3 @@ private:
   NetmaskGroup d_excludedSubnets;
 };
 
-#endif /* HAVE_EBPF */
index 50e95bf60a91f8996d45c4d3da7a5f6390723e8b..89c075f22ff48b5c8fc936b63f5c8cd67d4e6d2e 100644 (file)
@@ -396,7 +396,7 @@ void setupLuaBindings(LuaContext& luaCtx, bool client)
       setLuaNoSideEffect();
       std::string res;
       if (bpf) {
-        std::vector<std::pair<ComboAddress, uint64_t> > stats = bpf->getAddrStats();
+        auto stats = bpf->getAddrStats();
         for (const auto& value : stats) {
           if (value.first.sin4.sin_family == AF_INET) {
             res += value.first.toString() + ": " + std::to_string(value.second) + "\n";
@@ -405,7 +405,7 @@ void setupLuaBindings(LuaContext& luaCtx, bool client)
             res += "[" + value.first.toString() + "]: " + std::to_string(value.second) + "\n";
           }
         }
-        std::vector<std::tuple<DNSName, uint16_t, uint64_t> > qstats = bpf->getQNameStats();
+        auto qstats = bpf->getQNameStats();
         for (const auto& value : qstats) {
           res += std::get<0>(value).toString() + " " + std::to_string(std::get<1>(value)) + ": " + std::to_string(std::get<2>(value)) + "\n";
         }
index 6de9aed7716fda1cb65f538565f999fbc7f1b179..8e8158fafabf146596908a58b62ad2c1d866305e 100644 (file)
@@ -1149,7 +1149,11 @@ static void setupLuaConfig(LuaContext& luaCtx, bool client, bool configCheck)
       g_outputBuffer = (fmt % "What" % "Seconds" % "Blocks" % "Warning" % "Action" % "Reason").str();
       for(const auto& e: slow) {
         if (now < e.second.until) {
-          g_outputBuffer+= (fmt % e.first.toString() % (e.second.until.tv_sec - now.tv_sec) % e.second.blocks % (e.second.warning ? "true" : "false") % DNSAction::typeToString(e.second.action != DNSAction::Action::None ? e.second.action : g_dynBlockAction) % e.second.reason).str();
+          uint64_t counter = e.second.blocks;
+          if (g_defaultBPFFilter && e.second.bpf) {
+            counter += g_defaultBPFFilter->getHits(e.first.getNetwork());
+          }
+          g_outputBuffer+= (fmt % e.first.toString() % (e.second.until.tv_sec - now.tv_sec) % counter % (e.second.warning ? "true" : "false") % DNSAction::typeToString(e.second.action != DNSAction::Action::None ? e.second.action : g_dynBlockAction) % e.second.reason).str();
         }
       }
       auto slow2 = g_dynblockSMT.getCopy();
index e414ddbd29d9a23db9014fe09dacd3bc5eb4bf5e..05f58e82f94d26e365eda56441fe01a5e6eb5575 100644 (file)
@@ -99,10 +99,10 @@ string g_outputBuffer;
 std::vector<std::shared_ptr<TLSFrontend>> g_tlslocals;
 std::vector<std::shared_ptr<DOHFrontend>> g_dohlocals;
 std::vector<std::shared_ptr<DNSCryptContext>> g_dnsCryptLocals;
-#ifdef HAVE_EBPF
-shared_ptr<BPFFilter> g_defaultBPFFilter;
+
+shared_ptr<BPFFilter> g_defaultBPFFilter{nullptr};
 std::vector<std::shared_ptr<DynBPFFilter> > g_dynBPFFilters;
-#endif /* HAVE_EBPF */
+
 std::vector<std::unique_ptr<ClientState>> g_frontends;
 GlobalStateHolder<pools_t> g_pools;
 size_t g_udpVectorSize{1};
index 81c13458f6c440439ca812b3e8949339212a0d33..7c0699f2d0e410bd6f720fdded42c99f0d7516da 100644 (file)
@@ -207,12 +207,12 @@ struct DynBlock
   {
   }
 
-  DynBlock(const DynBlock& rhs): reason(rhs.reason), domain(rhs.domain), until(rhs.until), action(rhs.action), warning(rhs.warning)
+  DynBlock(const DynBlock& rhs): reason(rhs.reason), domain(rhs.domain), until(rhs.until), action(rhs.action), warning(rhs.warning), bpf(rhs.bpf)
   {
     blocks.store(rhs.blocks);
   }
 
-  DynBlock(DynBlock&& rhs): reason(std::move(rhs.reason)), domain(std::move(rhs.domain)), until(rhs.until), action(rhs.action), warning(rhs.warning)
+  DynBlock(DynBlock&& rhs): reason(std::move(rhs.reason)), domain(std::move(rhs.domain)), until(rhs.until), action(rhs.action), warning(rhs.warning), bpf(rhs.bpf)
   {
     blocks.store(rhs.blocks);
   }
@@ -225,6 +225,7 @@ struct DynBlock
     action = rhs.action;
     blocks.store(rhs.blocks);
     warning = rhs.warning;
+    bpf = rhs.bpf;
     return *this;
   }
 
@@ -236,6 +237,7 @@ struct DynBlock
     action = rhs.action;
     blocks.store(rhs.blocks);
     warning = rhs.warning;
+    bpf = rhs.bpf;
     return *this;
   }
 
@@ -245,6 +247,7 @@ struct DynBlock
   mutable std::atomic<unsigned int> blocks;
   DNSAction::Action action{DNSAction::Action::None};
   bool warning{false};
+  bool bpf{false};
 };
 
 extern GlobalStateHolder<NetmaskTree<DynBlock>> g_dynblockNMG;
@@ -734,7 +737,6 @@ struct ClientState
     return result;
   }
 
-#ifdef HAVE_EBPF
   shared_ptr<BPFFilter> d_filter;
 
   void detachFilter()
@@ -752,7 +754,6 @@ struct ClientState
     bpf->addSocket(getSocket());
     d_filter = bpf;
   }
-#endif /* HAVE_EBPF */
 
   void updateTCPMetrics(size_t nbQueries, uint64_t durationMs)
   {
@@ -1150,10 +1151,8 @@ extern size_t g_udpVectorSize;
 extern bool g_preserveTrailingData;
 extern bool g_allowEmptyResponse;
 
-#ifdef HAVE_EBPF
 extern shared_ptr<BPFFilter> g_defaultBPFFilter;
 extern std::vector<std::shared_ptr<DynBPFFilter> > g_dynBPFFilters;
-#endif /* HAVE_EBPF */
 
 struct LocalHolders
 {
index 574cd4c4c2fc66501307736a61a97b74c74904fd..d689fdb3c44b1e456bf9b1dccf5053741fd670cc 100644 (file)
@@ -176,8 +176,11 @@ void DynBlockRulesGroup::addOrRefreshBlock(boost::optional<NetmaskTree<DynBlock>
   const auto& got = blocks->lookup(Netmask(requestor));
   bool expired = false;
   bool wasWarning = false;
+  bool bpf = false;
 
   if (got) {
+    bpf = got->second.bpf;
+
     if (warning && !got->second.warning) {
       /* we have an existing entry which is not a warning,
          don't override it */
@@ -205,10 +208,26 @@ void DynBlockRulesGroup::addOrRefreshBlock(boost::optional<NetmaskTree<DynBlock>
   DynBlock db{rule.d_blockReason, until, DNSName(), warning ? DNSAction::Action::NoOp : rule.d_action};
   db.blocks = count;
   db.warning = warning;
-  if (!d_beQuiet && (!got || expired || wasWarning)) {
-    warnlog("Inserting %sdynamic block for %s for %d seconds: %s", warning ? "(warning) " :"", requestor.toString(), rule.d_blockDuration, rule.d_blockReason);
+  if (!got || expired || wasWarning) {
+    if (g_defaultBPFFilter) {
+      try {
+        g_defaultBPFFilter->block(requestor);
+        bpf = true;
+      }
+      catch (const std::exception& e) {
+        vinfolog("Unable to insert eBPF dynamic block for %s, falling back to regular dynamic block: %s", requestor.toString(), e.what());
+      }
+    }
+
+    if (!d_beQuiet) {
+      warnlog("Inserting %sdynamic block for %s for %d seconds: %s", warning ? "(warning) " :"", requestor.toString(), rule.d_blockDuration, rule.d_blockReason);
+    }
   }
-  blocks->insert(Netmask(requestor)).second = db;
+
+  db.bpf = bpf;
+
+  blocks->insert(Netmask(requestor)).second = std::move(db);
+
   updated = true;
 }
 
@@ -371,6 +390,14 @@ void DynBlockMaintenance::purgeExpired(const struct timespec& now)
     for (const auto& entry : *blocks) {
       if (!(now < entry.second.until)) {
         toRemove.push_back(entry.first);
+        if (g_defaultBPFFilter && entry.second.bpf) {
+          try {
+            g_defaultBPFFilter->unblock(entry.first.getNetwork());
+          }
+          catch (const std::exception& e) {
+            vinfolog("Error while removing eBPF dynamic block for %s: %s", entry.first.toString(), e.what());
+          }
+        }
       }
     }
     if (!toRemove.empty()) {
@@ -410,8 +437,14 @@ std::map<std::string, std::list<std::pair<Netmask, unsigned int>>> DynBlockMaint
   auto blocks = g_dynblockNMG.getLocal();
   for (const auto& entry : *blocks) {
     auto& topsForReason = results[entry.second.reason];
-    if (topsForReason.size() < topN || topsForReason.front().second < entry.second.blocks) {
-      auto newEntry = std::make_pair(entry.first, entry.second.blocks.load());
+    uint64_t value = entry.second.blocks.load();
+
+    if (g_defaultBPFFilter && entry.second.bpf) {
+      value += g_defaultBPFFilter->getHits(entry.first.getNetwork());
+    }
+
+    if (topsForReason.size() < topN || topsForReason.front().second < value) {
+      auto newEntry = std::make_pair(entry.first, value);
 
       if (topsForReason.size() >= topN) {
         topsForReason.pop_front();
index 1b853145cc31c4f86fb98d6d58bff067e04f6be3..75d59cf023b2d778e53369879865b90ba173ac4b 100644 (file)
@@ -60,4 +60,6 @@ The dynamic eBPF blocks and the number of queries they blocked can be seen in th
 
 They can be unregistered at a later point using the :func:`unregisterDynBPFFilter` function.
 
+Since 1.6.0, the default BPF filter set via :func:`setDefaultBPFFilter` will automatically get used when a dynamic block is inserted via a :ref:`DynBlockRulesGroup`.
+
 This feature has been successfully tested on Arch Linux, Arch Linux ARM, Fedora Core 23 and Ubuntu Xenial
index 8b3ed9186f5360a07e24ab23bc38990290dbff6e..4936cdc465e56fc0a827c8eaa57b168043161df1 100644 (file)
@@ -85,3 +85,4 @@ action is applied.
   -- If the query rate raises above 300 qps for 10 seconds, we'll block the client for 60s.
   dbr:setQueryRate(300, 10, "Exceeded query rate", 60, DNSAction.Drop, 100)
 
+Since 1.6.0, if a default eBPF filter has been set via :func:`setDefaultBPFFilter` dnsdist will automatically try to use it when a dynamic block is inserted via a :ref:`DynBlockRulesGroup`. eBPF blocks are applied in kernel space and are much more efficient than user space ones. Note that a regular block is also inserted so that any failure will result in a regular block being used instead of the eBPF one.
index caee5adcff94b356f684dca202511bbc832a0f00..6de07d5ff94f8b7ba5b32a9436f168b7b39f3b38 100644 (file)
@@ -11,6 +11,7 @@
 Rings g_rings;
 GlobalStateHolder<NetmaskTree<DynBlock>> g_dynblockNMG;
 GlobalStateHolder<SuffixMatchTree<DynBlock>> g_dynblockSMT;
+shared_ptr<BPFFilter> g_defaultBPFFilter{nullptr};
 
 BOOST_AUTO_TEST_SUITE(dnsdistdynblocks_hh)