diff options
Diffstat (limited to '')
-rw-r--r-- | dnsdist-dynblocks.hh | 452 |
1 files changed, 452 insertions, 0 deletions
diff --git a/dnsdist-dynblocks.hh b/dnsdist-dynblocks.hh new file mode 100644 index 0000000..c9b1e4a --- /dev/null +++ b/dnsdist-dynblocks.hh @@ -0,0 +1,452 @@ +/* + * 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 + +#ifndef DISABLE_DYNBLOCKS +#include <unordered_set> + +#include "dolog.hh" +#include "dnsdist-rings.hh" +#include "statnode.hh" + +extern "C" { +#include "dnsdist-lua-inspection-ffi.h" +} + +// dnsdist_ffi_stat_node_t is a lightuserdata +template<> +struct LuaContext::Pusher<dnsdist_ffi_stat_node_t*> { + static const int minSize = 1; + static const int maxSize = 1; + + static PushedObject push(lua_State* state, dnsdist_ffi_stat_node_t* ptr) noexcept { + lua_pushlightuserdata(state, ptr); + return PushedObject{state, 1}; + } +}; + +typedef std::function<bool(dnsdist_ffi_stat_node_t*)> dnsdist_ffi_stat_node_visitor_t; + +struct dnsdist_ffi_stat_node_t +{ + dnsdist_ffi_stat_node_t(const StatNode& node_, const StatNode::Stat& self_, const StatNode::Stat& children_, std::optional<std::string>& reason_): node(node_), self(self_), children(children_), reason(reason_) + { + } + + const StatNode& node; + const StatNode::Stat& self; + const StatNode::Stat& children; + std::optional<std::string>& reason; +}; + +class DynBlockRulesGroup +{ +private: + + struct Counts + { + std::map<uint8_t, uint64_t> d_rcodeCounts; + std::map<uint16_t, uint64_t> d_qtypeCounts; + uint64_t queries{0}; + uint64_t responses{0}; + uint64_t respBytes{0}; + }; + + struct DynBlockRule + { + DynBlockRule(): d_enabled(false) + { + } + + DynBlockRule(const std::string& blockReason, unsigned int blockDuration, unsigned int rate, unsigned int warningRate, unsigned int seconds, DNSAction::Action action): d_blockReason(blockReason), d_blockDuration(blockDuration), d_rate(rate), d_warningRate(warningRate), d_seconds(seconds), d_action(action), d_enabled(true) + { + } + + bool matches(const struct timespec& when) + { + if (!d_enabled) { + return false; + } + + if (d_seconds && when < d_cutOff) { + return false; + } + + if (when < d_minTime) { + d_minTime = when; + } + + return true; + } + + bool rateExceeded(unsigned int count, const struct timespec& now) const + { + if (!d_enabled) { + return false; + } + + double delta = d_seconds ? d_seconds : DiffTime(now, d_minTime); + double limit = delta * d_rate; + return (count > limit); + } + + bool warningRateExceeded(unsigned int count, const struct timespec& now) const + { + if (!d_enabled) { + return false; + } + + if (d_warningRate == 0) { + return false; + } + + double delta = d_seconds ? d_seconds : DiffTime(now, d_minTime); + double limit = delta * d_warningRate; + return (count > limit); + } + + bool isEnabled() const + { + return d_enabled; + } + + std::string toString() const + { + if (!isEnabled()) { + return ""; + } + + std::stringstream result; + if (d_action != DNSAction::Action::None) { + result << DNSAction::typeToString(d_action) << " "; + } + else { + result << "Apply the global DynBlock action "; + } + result << "for " << std::to_string(d_blockDuration) << " seconds when over " << std::to_string(d_rate) << " during the last " << d_seconds << " seconds, reason: '" << d_blockReason << "'"; + + return result.str(); + } + + std::string d_blockReason; + struct timespec d_cutOff; + struct timespec d_minTime; + unsigned int d_blockDuration{0}; + unsigned int d_rate{0}; + unsigned int d_warningRate{0}; + unsigned int d_seconds{0}; + DNSAction::Action d_action{DNSAction::Action::None}; + bool d_enabled{false}; + }; + + struct DynBlockRatioRule: DynBlockRule + { + DynBlockRatioRule(): DynBlockRule() + { + } + + DynBlockRatioRule(const std::string& blockReason, unsigned int blockDuration, double ratio, double warningRatio, unsigned int seconds, DNSAction::Action action, size_t minimumNumberOfResponses): DynBlockRule(blockReason, blockDuration, 0, 0, seconds, action), d_minimumNumberOfResponses(minimumNumberOfResponses), d_ratio(ratio), d_warningRatio(warningRatio) + { + } + + bool ratioExceeded(unsigned int total, unsigned int count) const + { + if (!d_enabled) { + return false; + } + + if (total < d_minimumNumberOfResponses) { + return false; + } + + double allowed = d_ratio * static_cast<double>(total); + return (count > allowed); + } + + bool warningRatioExceeded(unsigned int total, unsigned int count) const + { + if (!d_enabled) { + return false; + } + + if (d_warningRatio == 0.0) { + return false; + } + + if (total < d_minimumNumberOfResponses) { + return false; + } + + double allowed = d_warningRatio * static_cast<double>(total); + return (count > allowed); + } + + std::string toString() const + { + if (!isEnabled()) { + return ""; + } + + std::stringstream result; + if (d_action != DNSAction::Action::None) { + result << DNSAction::typeToString(d_action) << " "; + } + else { + result << "Apply the global DynBlock action "; + } + result << "for " << std::to_string(d_blockDuration) << " seconds when over " << std::to_string(d_ratio) << " ratio during the last " << d_seconds << " seconds, reason: '" << d_blockReason << "'"; + + return result.str(); + } + + size_t d_minimumNumberOfResponses{0}; + double d_ratio{0.0}; + double d_warningRatio{0.0}; + }; + + typedef std::unordered_map<AddressAndPortRange, Counts, AddressAndPortRange::hash> counts_t; + +public: + DynBlockRulesGroup() + { + } + + void setQueryRate(unsigned int rate, unsigned int warningRate, unsigned int seconds, const std::string& reason, unsigned int blockDuration, DNSAction::Action action) + { + d_queryRateRule = DynBlockRule(reason, blockDuration, rate, warningRate, seconds, action); + } + + /* rate is in bytes per second */ + void setResponseByteRate(unsigned int rate, unsigned int warningRate, unsigned int seconds, const std::string& reason, unsigned int blockDuration, DNSAction::Action action) + { + d_respRateRule = DynBlockRule(reason, blockDuration, rate, warningRate, seconds, action); + } + + void setRCodeRate(uint8_t rcode, unsigned int rate, unsigned int warningRate, unsigned int seconds, const std::string& reason, unsigned int blockDuration, DNSAction::Action action) + { + auto& entry = d_rcodeRules[rcode]; + entry = DynBlockRule(reason, blockDuration, rate, warningRate, seconds, action); + } + + void setRCodeRatio(uint8_t rcode, double ratio, double warningRatio, unsigned int seconds, const std::string& reason, unsigned int blockDuration, DNSAction::Action action, size_t minimumNumberOfResponses) + { + auto& entry = d_rcodeRatioRules[rcode]; + entry = DynBlockRatioRule(reason, blockDuration, ratio, warningRatio, seconds, action, minimumNumberOfResponses); + } + + void setQTypeRate(uint16_t qtype, unsigned int rate, unsigned int warningRate, unsigned int seconds, const std::string& reason, unsigned int blockDuration, DNSAction::Action action) + { + auto& entry = d_qtypeRules[qtype]; + entry = DynBlockRule(reason, blockDuration, rate, warningRate, seconds, action); + } + + typedef std::function<std::tuple<bool, boost::optional<std::string>>(const StatNode&, const StatNode::Stat&, const StatNode::Stat&)> smtVisitor_t; + + void setSuffixMatchRule(unsigned int seconds, const std::string& reason, unsigned int blockDuration, DNSAction::Action action, smtVisitor_t visitor) + { + d_suffixMatchRule = DynBlockRule(reason, blockDuration, 0, 0, seconds, action); + d_smtVisitor = visitor; + } + + void setSuffixMatchRuleFFI(unsigned int seconds, const std::string& reason, unsigned int blockDuration, DNSAction::Action action, dnsdist_ffi_stat_node_visitor_t visitor) + { + d_suffixMatchRule = DynBlockRule(reason, blockDuration, 0, 0, seconds, action); + d_smtVisitorFFI = visitor; + } + + void setMasks(uint8_t v4, uint8_t v6, uint8_t port) + { + d_v4Mask = v4; + d_v6Mask = v6; + d_portMask = port; + } + + void apply() + { + struct timespec now; + gettime(&now); + + apply(now); + } + + void apply(const struct timespec& now); + + void excludeRange(const Netmask& range) + { + d_excludedSubnets.addMask(range); + } + + void excludeRange(const NetmaskGroup& group) + { + d_excludedSubnets.addMasks(group, true); + } + + void includeRange(const Netmask& range) + { + d_excludedSubnets.addMask(range, false); + } + + void includeRange(const NetmaskGroup& group) + { + d_excludedSubnets.addMasks(group, false); + } + + void removeRange(const Netmask& range) + { + d_excludedSubnets.deleteMask(range); + } + + void removeRange(const NetmaskGroup& group) + { + d_excludedSubnets.deleteMasks(group); + } + + void excludeDomain(const DNSName& domain) + { + d_excludedDomains.add(domain); + } + + std::string toString() const + { + std::stringstream result; + + result << "Query rate rule: " << d_queryRateRule.toString() << std::endl; + result << "Response rate rule: " << d_respRateRule.toString() << std::endl; + result << "SuffixMatch rule: " << d_suffixMatchRule.toString() << std::endl; + result << "RCode rules: " << std::endl; + for (const auto& rule : d_rcodeRules) { + result << "- " << RCode::to_s(rule.first) << ": " << rule.second.toString() << std::endl; + } + for (const auto& rule : d_rcodeRatioRules) { + result << "- " << RCode::to_s(rule.first) << ": " << rule.second.toString() << std::endl; + } + result << "QType rules: " << std::endl; + for (const auto& rule : d_qtypeRules) { + result << "- " << QType(rule.first).toString() << ": " << rule.second.toString() << std::endl; + } + result << "Excluded Subnets: " << d_excludedSubnets.toString() << std::endl; + result << "Excluded Domains: " << d_excludedDomains.toString() << std::endl; + + return result.str(); + } + + void setQuiet(bool quiet) + { + d_beQuiet = quiet; + } + +private: + + bool checkIfQueryTypeMatches(const Rings::Query& query); + bool checkIfResponseCodeMatches(const Rings::Response& response); + void addOrRefreshBlock(boost::optional<NetmaskTree<DynBlock, AddressAndPortRange> >& blocks, const struct timespec& now, const AddressAndPortRange& requestor, const DynBlockRule& rule, bool& updated, bool warning); + void addOrRefreshBlockSMT(SuffixMatchTree<DynBlock>& blocks, const struct timespec& now, const DNSName& name, const DynBlockRule& rule, bool& updated); + + void addBlock(boost::optional<NetmaskTree<DynBlock, AddressAndPortRange> >& blocks, const struct timespec& now, const AddressAndPortRange& requestor, const DynBlockRule& rule, bool& updated) + { + addOrRefreshBlock(blocks, now, requestor, rule, updated, false); + } + + void handleWarning(boost::optional<NetmaskTree<DynBlock, AddressAndPortRange> >& blocks, const struct timespec& now, const AddressAndPortRange& requestor, const DynBlockRule& rule, bool& updated) + { + addOrRefreshBlock(blocks, now, requestor, rule, updated, true); + } + + bool hasQueryRules() const + { + return d_queryRateRule.isEnabled() || !d_qtypeRules.empty(); + } + + bool hasResponseRules() const + { + return d_respRateRule.isEnabled() || !d_rcodeRules.empty() || !d_rcodeRatioRules.empty(); + } + + bool hasSuffixMatchRules() const + { + return d_suffixMatchRule.isEnabled(); + } + + bool hasRules() const + { + return hasQueryRules() || hasResponseRules(); + } + + void processQueryRules(counts_t& counts, const struct timespec& now); + void processResponseRules(counts_t& counts, StatNode& root, const struct timespec& now); + + std::map<uint8_t, DynBlockRule> d_rcodeRules; + std::map<uint8_t, DynBlockRatioRule> d_rcodeRatioRules; + std::map<uint16_t, DynBlockRule> d_qtypeRules; + DynBlockRule d_queryRateRule; + DynBlockRule d_respRateRule; + DynBlockRule d_suffixMatchRule; + NetmaskGroup d_excludedSubnets; + SuffixMatchNode d_excludedDomains; + smtVisitor_t d_smtVisitor; + dnsdist_ffi_stat_node_visitor_t d_smtVisitorFFI; + uint8_t d_v6Mask{128}; + uint8_t d_v4Mask{32}; + uint8_t d_portMask{0}; + bool d_beQuiet{false}; +}; + +class DynBlockMaintenance +{ +public: + static void run(); + + /* return the (cached) number of hits per second for the top offenders, averaged over 60s */ + static std::map<std::string, std::list<std::pair<AddressAndPortRange, unsigned int>>> getHitsForTopNetmasks(); + static std::map<std::string, std::list<std::pair<DNSName, unsigned int>>> getHitsForTopSuffixes(); + + /* get the the top offenders based on the current value of the counters */ + static std::map<std::string, std::list<std::pair<AddressAndPortRange, unsigned int>>> getTopNetmasks(size_t topN); + static std::map<std::string, std::list<std::pair<DNSName, unsigned int>>> getTopSuffixes(size_t topN); + static void purgeExpired(const struct timespec& now); + + static time_t s_expiredDynBlocksPurgeInterval; + +private: + static void collectMetrics(); + static void generateMetrics(); + + struct MetricsSnapshot + { + std::map<std::string, std::list<std::pair<AddressAndPortRange, unsigned int>>> nmgData; + std::map<std::string, std::list<std::pair<DNSName, unsigned int>>> smtData; + }; + + struct Tops + { + std::map<std::string, std::list<std::pair<AddressAndPortRange, unsigned int>>> topNMGsByReason; + std::map<std::string, std::list<std::pair<DNSName, unsigned int>>> topSMTsByReason; + }; + + static LockGuarded<Tops> s_tops; + /* s_metricsData should only be accessed by the dynamic blocks maintenance thread so it does not need a lock */ + // need N+1 datapoints to be able to do the diff after a collection point has been reached + static std::list<MetricsSnapshot> s_metricsData; + static size_t s_topN; +}; + +#endif /* DISABLE_DYNBLOCKS */ |