Server pools
------------
-
Now for some cool stuff. Let's say we know we're getting a whole bunch of
traffic for a domain used in DoS attacks, for example 'sh43354.cn'. We can
do two things with this kind of traffic. Either we block it outright, like
> addPoolRule({"192.168.12.0/24", "192.168.13.14"}, "abuse")
```
-Pool rules can be inspected with `showPoolRules()`, and can be deleted with
-`rmPoolRule()`. Servers can be added or removed to pools with:
+Both `addDomainBlock` and `addPoolRule` end up the list of Rules
+and Actions (for which see below).
+
+Servers can be added or removed to pools with:
```
> getServer(7):addPool("abuse")
> getServer(4):rmPool("abuse")
```
+
+Rules
+-----
+Rules can be inspected with `showRules()`, and can be deleted with
+`rmRule()`. Rules are evaluated in order, and this order can be changed
+with `mvRule(from, to)` (see below for exact semantics).
+
+Rules have selectors and actions. Current selectors are:
+ * Source address
+ * Query type
+ * Query domain
+
+Current actions are:
+ * Drop
+ * Route to a pool
+ * Return with TC=1 (truncated, ie, instruction to retry with TCP)
+ * Force a ServFail, NotImp or Refused answer
+ * Send out a crafted response (NXDOMAIN or "real" data)
+
More power
----------
More powerful things can be achieved by defining a function called
TCP/IP, and in this way you can implement ANY-to-TCP even for downstream
servers that lack this feature.
+Note that calling `addAnyTCRule()` achieves the same thing, without
+involving Lua.
+
Inspecting live traffic
-----------------------
This is still much in flux, but for now, try:
> addQPSLimit("h4xorbooter.xyz.", 10)
> addQPSLimit({"130.161.0.0/16", "145.14.0.0/16"} , 20)
> addQPSLimit({"nl.", "be."}, 1)
-> showQPSLimits()
-# Object Lim Passed Blocked
-0 h4xorbooter.xyz. 10 0 0
-1 130.161.0.0/16, 145.14.0.0/16 20 0 0
-2 nl., be. 1 2 8
+> showRules()
+# Matches Rule Action
+0 0 h4xorbooter.xyz. qps limit to 10
+1 0 130.161.0.0/16, 145.14.0.0/16 qps limit to 20
+2 0 nl., be. qps limit to 1
```
-To delete a limit:
+To delete a limit (or a rule in general):
```
-> deleteQPSLimit(1)
-> showQPSLimits()
-# Object Lim Passed Blocked
-0 h4xorbooter.xyz. 10 0 0
-1 nl., be. 1 16 251
+> rmRule(1)
+> showRules()
+# Matches Rule Action
+0 0 h4xorbooter.xyz. qps limit to 10
+1 0 nl., be. qps limit to 1
```
Dynamic load balancing
----------------------
-
The default load balancing policy is called 'leastOutstanding', which means
we pick the server with the least queries 'in the air'.
function splitSetup(servers, remote, qname, qtype, dh)
if(dh:getRD() == false)
then
- return authServer
+ return leastOutstanding(getPoolServers("auth"), remote, qname, qtype, dh)
else
return leastOutstanding(servers, remote, qname, qtype, dh)
end
setServerPolicyLua("splitsetup", splitSetup)
```
-This will forward queries that don't want recursion to a specific
-server, and will apply the default load balancing policy to all
-other queries.
+This will forward queries that don't want recursion to the pool of auth
+servers, and will apply the default load balancing policy to all other
+queries.
Running it for real
-------------------
Here are all functions:
+ * Practical
+ * `shutdown()`: shut down dnsdist
+ * quit or ^D: exit the console
* ACL related:
* `addACL(netmask)`: add to the ACL set who can use this server
* `setACL({netmask, netmask})`: replace the ACL set with these netmasks. Use `setACL({})` to reset the list, meaning no one can use us
* `upStatus`: if dnsdist considers this server available (overridden by `setDown()` and `setUp()`)
* `order`: order of this server in order-based server selection policies
* `weight`: weight of this server in weighted server selection policies
+ * Rule related:
+ * `showRules()`: show all defined rules (Pool, Block, QPS, addAnyTCRule)
+ * `rmRule(n)`: remove rule n
+ * `mvRule(from, to)`: move rule 'from' to a position where it is in front of 'to'. 'to' can be one larger than the largest rule,
+ in which case the rule will be moved to the last position.
* Pool related:
* `addPoolRule(domain, pool)`: send queries to this domain to that pool
* `addPoolRule({domain, domain}, pool)`: send queries to these domains to that pool
* `addPoolRule(netmask, pool)`: send queries to this netmask to that pool
* `addPoolRule({netmask, netmask}, pool)`: send queries to these netmasks to that pool
- * `rmPoolRule(n)`: remove rule n
- * `showPoolRules()`: show the pool rules
* `getPoolServers(pool)`: return servers part of this pool
* Server selection policy related:
* `setServerPolicy(policy)`: set server selection policy to that policy
* `addQPSLimit({domain, domain}, n)`: limit queries within those domains (together) to n per second
* `addQPSLimit(netmask, n)`: limit queries within that netmask to n per second
* `addQPSLimit({netmask, netmask}, n)`: limit queries within those netmasks (together) to n per second
- * `rmQPSLimit(n)`: remove QPS limit n
- * `showQPSLimits()`: outputs QPS limits
* Advanced functions for writing your own policies and hooks
* ComboAddress related:
* `tostring()`: return in human-friendly format
#include "dnsdist.hh"
+#include "dnsrulactions.hh"
#include <thread>
#include "dolog.hh"
#include "sodcrypto.hh"
return ret;
} );
+ g_lua.writeFunction("addAnyTCRule", []() {
+ auto rules=g_rulactions.getCopy();
+ rules.push_back({ std::make_shared<QTypeRule>(0xff), std::make_shared<TCAction>()});
+ g_rulactions.setState(rules);
+ });
+ g_lua.writeFunction("rmRule", [](unsigned int num) {
+ auto rules = g_rulactions.getCopy();
+ if(num >= rules.size()) {
+ g_outputBuffer = "Error: attempt to delete non-existing rule\n";
+ return;
+ }
+ rules.erase(rules.begin()+num);
+ g_rulactions.setState(rules);
+ });
+
+ g_lua.writeFunction("mvRule", [](unsigned int from, unsigned int to) {
+ auto rules = g_rulactions.getCopy();
+ if(from >= rules.size() || to > rules.size()) {
+ g_outputBuffer = "Error: attempt to move rules from/to invalid index\n";
+ return;
+ }
+ if(from==to)
+ return;
+
+
+ auto subject = rules[from];
+ rules.erase(rules.begin()+from);
+ if(to == rules.size())
+ rules.push_back(subject);
+ else {
+ if(from < to)
+ --to;
+ rules.insert(rules.begin()+to, subject);
+ }
+ g_rulactions.setState(rules);
+ });
g_lua.writeFunction("rmServer",
g_lua.writeFunction("addDomainBlock", [](const std::string& domain) {
- g_suffixMatchNodeFilter.modify([domain](SuffixMatchNode& smn) {
- smn.add(DNSName(domain));
- });
+ SuffixMatchNode smn;
+ smn.add(domain);
+ g_rulactions.modify([smn](decltype(g_rulactions)::value_type& rulactions) {
+ rulactions.push_back({
+ std::make_shared<SuffixMatchNodeRule>(smn),
+ std::make_shared<DropAction>() });
+ });
+
});
g_lua.writeFunction("showServers", []() {
try {
}
}
if(nmg.empty())
- g_poolrules.modify([smn, pool](decltype(g_poolrules)::value_type& poolrules) {
- poolrules.push_back({smn, pool});
+ g_rulactions.modify([smn, pool](decltype(g_rulactions)::value_type& rulactions) {
+ rulactions.push_back({
+ std::make_shared<SuffixMatchNodeRule>(smn),
+ std::make_shared<PoolAction>(pool) });
});
else
- g_poolrules.modify([nmg,pool](decltype(g_poolrules)::value_type& poolrules) {
- poolrules.push_back({nmg, pool});
+ g_rulactions.modify([nmg,pool](decltype(g_rulactions)::value_type& rulactions) {
+ rulactions.push_back({std::make_shared<NetmaskGroupRule>(nmg),
+ std::make_shared<PoolAction>(pool)});
});
});
- g_lua.writeFunction("showPoolRules", []() {
- boost::format fmt("%-3d %-50s %s\n");
- g_outputBuffer += (fmt % "#" % "Object" % "Pool").str();
- int num=0;
- for(const auto& lim : g_poolrules.getCopy()) {
- string name;
- if(auto nmg=boost::get<NetmaskGroup>(&lim.first)) {
- name=nmg->toString();
- }
- else if(auto smn=boost::get<SuffixMatchNode>(&lim.first)) {
- name=smn->toString();
- }
- g_outputBuffer += (fmt % num % name % lim.second).str();
- ++num;
- }
- });
-
g_lua.writeFunction("addQPSLimit", [](boost::variant<string,vector<pair<int, string>> > var, int lim) {
SuffixMatchNode smn;
}
}
if(nmg.empty())
- g_limiters.modify([smn, lim](decltype(g_limiters)::value_type& limiters) {
- limiters.push_back({smn, QPSLimiter(lim, lim)});
+ g_rulactions.modify([smn, lim](decltype(g_rulactions)::value_type& rulactions) {
+ rulactions.push_back({std::make_shared<SuffixMatchNodeRule>(smn),
+ std::make_shared<QPSAction>(lim)});
});
else
- g_limiters.modify([nmg, lim](decltype(g_limiters)::value_type& limiters) {
- limiters.push_back({nmg, QPSLimiter(lim, lim)});
+ g_rulactions.modify([nmg, lim](decltype(g_rulactions)::value_type& rulactions) {
+ rulactions.push_back({std::make_shared<NetmaskGroupRule>(nmg),
+ std::make_shared<QPSAction>(lim)});
});
});
- g_lua.writeFunction("rmQPSLimit", [](int i) {
- g_limiters.modify([i](decltype(g_limiters)::value_type& limiters) {
- limiters.erase(limiters.begin() + i);
- });
- });
- g_lua.writeFunction("showQPSLimits", []() {
- boost::format fmt("%-3d %-50s %7d %8d %8d\n");
- g_outputBuffer += (fmt % "#" % "Object" % "Lim" % "Passed" % "Blocked").str();
- int num=0;
- for(const auto& lim : g_limiters.getCopy()) {
- string name;
- if(auto nmg=boost::get<NetmaskGroup>(&lim.first)) {
- name=nmg->toString();
- }
- else if(auto smn=boost::get<SuffixMatchNode>(&lim.first)) {
- name=smn->toString();
- }
- g_outputBuffer += (fmt % num % name % lim.second.getRate() % lim.second.getPassed() % lim.second.getBlocked()).str();
+ g_lua.writeFunction("showRules", []() {
+ boost::format fmt("%-3d %9d %-50s %s\n");
+ g_outputBuffer += (fmt % "#" % "Matches" % "Rule" % "Action").str();
+ int num=0;
+ for(const auto& lim : g_rulactions.getCopy()) {
+ string name = lim.first->toString();
+ g_outputBuffer += (fmt % num % lim.first->d_matches % name % lim.second->toString()).str();
++num;
}
});
-
g_lua.writeFunction("getServers", []() {
vector<pair<int, std::shared_ptr<DownstreamState> > > ret;
int count=1;
#include "base64.hh"
#include <fstream>
#include "sodcrypto.hh"
+#include "dnsrulactions.hh"
#undef L
/* Known sins:
lack of help()
lack of autocomplete
TCP is a bit wonky and may pick the wrong downstream
+ ringbuffers are on a wing & a prayer because partially unlocked
*/
+/* the Rulaction plan
+ Set of Rules, if one matches, it leads to an Action
+ Both rules and actions could conceivably be Lua based.
+ On the C++ side, both could be inherited from a class Rule and a class Action,
+ on the Lua side we can't do that. */
+
namespace po = boost::program_options;
po::variables_map g_vm;
using std::atomic;
If all downstreams are over QPS, we pick the fastest server */
-GlobalStateHolder<vector<pair<boost::variant<SuffixMatchNode,NetmaskGroup>, QPSLimiter> > > g_limiters;
-GlobalStateHolder<vector<pair<boost::variant<SuffixMatchNode,NetmaskGroup>, string> > > g_poolrules;
+GlobalStateHolder<vector<pair<std::shared_ptr<DNSRule>, std::shared_ptr<DNSAction> > > > g_rulactions;
Rings g_rings;
GlobalStateHolder<servers_t> g_dstates;
}
}
-GlobalStateHolder<SuffixMatchNode> g_suffixMatchNodeFilter;
-
ComboAddress g_serverControl{"127.0.0.1:5199"};
return ret;
}
+
+
// listens to incoming queries, sends out to downstream servers, noting the intended return path
void* udpClientThread(ClientState* cs)
try
}
auto acl = g_ACL.getLocal();
auto localPolicy = g_policy.getLocal();
- auto localLimiters = g_limiters.getLocal();
- auto localPool = g_poolrules.getLocal();
- auto localMatchNodeFilter = g_suffixMatchNodeFilter.getLocal();
+
+ auto localRulactions = g_rulactions.getLocal();
auto localServers = g_dstates.getLocal();
struct msghdr msgh;
struct iovec iov;
DNSName qname(packet, len, 12, false, &qtype);
g_rings.queryRing.push_back(qname);
-
- bool blocked=false;
- for(const auto& lim : *localLimiters) {
- if(auto nmg=boost::get<NetmaskGroup>(&lim.first)) {
- if(nmg->match(remote) && !lim.second.check()) {
- blocked=true;
- break;
- }
- }
- else if(auto smn=boost::get<SuffixMatchNode>(&lim.first)) {
- if(smn->check(qname) && !lim.second.check()) {
- blocked=true;
- break;
- }
- }
- }
- if(blocked)
- continue;
-
-
-
+
if(blockFilter) {
std::lock_guard<std::mutex> lock(g_luamutex);
continue;
}
- if(localMatchNodeFilter->check(qname))
+
+ DNSAction::Action action=DNSAction::Action::None;
+ string ruleresult;
+ string pool;
+ for(const auto& lr : *localRulactions) {
+ if(lr.first->matches(remote, qname, qtype, dh)) {
+ lr.first->d_matches++;
+ action=(*lr.second)(remote, qname, qtype, dh, &ruleresult);
+ if(action != DNSAction::Action::None)
+ break;
+ }
+ }
+ switch(action) {
+ case DNSAction::Action::Drop:
continue;
-
+ case DNSAction::Action::Nxdomain:
+ dh->rcode = RCode::NXDomain;
+ dh->qr=true;
+ break;
+ case DNSAction::Action::Pool:
+ pool=ruleresult;
+ break;
+
+ case DNSAction::Action::Spoof:
+ ;
+ case DNSAction::Action::HeaderModify:
+ dh->qr=true;
+ break;
+ case DNSAction::Action::Allow:
+ case DNSAction::Action::None:
+ break;
+ }
+
if(dh->qr) { // something turned it into a response
ComboAddress dest;
if(HarvestDestinationAddress(&msgh, &dest))
continue;
}
- string pool;
- for(const auto& pr : *localPool) {
- if(auto nmg=boost::get<NetmaskGroup>(&pr.first)) {
- if(nmg->match(remote)) {
- pool=pr.second;
- break;
- }
- }
- else if(auto smn=boost::get<SuffixMatchNode>(&pr.first)) {
- if(smn->check(qname)) {
- pool=pr.second;
- break;
- }
- }
- }
DownstreamState* ss = 0;
auto candidates=getDownstreamCandidates(*localServers, pool);
auto policy=localPolicy->policy;
extern LuaContext g_lua;
extern std::string g_outputBuffer; // locking for this is ok, as locked by g_luamutex
+class DNSRule
+{
+public:
+ virtual bool matches(const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh) const =0;
+ virtual string toString() const = 0;
+ mutable std::atomic<uint64_t> d_matches{0};
+};
+
+/* so what could you do:
+ drop,
+ fake up nxdomain,
+ provide actual answer,
+ allow & and stop processing,
+ continue processing,
+ modify header: (servfail|refused|notimp), set TC=1,
+ send to pool */
+
+class DNSAction
+{
+public:
+ enum class Action { Drop, Nxdomain, Spoof, Allow, HeaderModify, Pool, None};
+ virtual Action operator()(const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh, string* ruleresult) const =0;
+ virtual string toString() const = 0;
+};
+
using NumberedServerVector = NumberedVector<shared_ptr<DownstreamState>>;
typedef std::function<shared_ptr<DownstreamState>(const NumberedServerVector& servers, const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh)> policy_t;
extern GlobalStateHolder<ServerPolicy> g_policy;
extern GlobalStateHolder<servers_t> g_dstates;
-extern GlobalStateHolder<vector<pair<boost::variant<SuffixMatchNode,NetmaskGroup>, QPSLimiter> >> g_limiters;
-extern GlobalStateHolder<vector<pair<boost::variant<SuffixMatchNode,NetmaskGroup>, std::string> >> g_poolrules;
-extern GlobalStateHolder<SuffixMatchNode> g_suffixMatchNodeFilter;
+extern GlobalStateHolder<vector<pair<std::shared_ptr<DNSRule>, std::shared_ptr<DNSAction> > > > g_rulactions;
extern GlobalStateHolder<NetmaskGroup> g_ACL;
extern ComboAddress g_serverControl; // not changed during runtime
std::shared_ptr<DownstreamState> leastOutstanding(const NumberedServerVector& servers, const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh);
std::shared_ptr<DownstreamState> wrandom(const NumberedServerVector& servers, const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh);
std::shared_ptr<DownstreamState> roundrobin(const NumberedServerVector& servers, const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh);
+
+template<typename T, typename... Args>
+std::unique_ptr<T> make_unique(Args&&... args)
+{
+ return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
+}
addPoolRule({"ezdns.it.", "xxx."}, "abuse")
addPoolRule("192.168.1.0/24", "abuse")
+addDomainBlock("powerdns.org.")
+addDomainBlock("spectre.")
+addDomainBlock("isis.")
+
block=newDNSName("powerdns.org.")
-- called before we distribute a question
function blockFilter(remote, qname, qtype, dh)
ragel $< -o dnslabeltext.cc
-EXTRA_DIST=dnslabeltext.rl dnsdistconf.lua
+EXTRA_DIST=dnslabeltext.rl dnsdistconf.lua README.md
bin_PROGRAMS = dnsdist
dnsdist_SOURCES = \
dnslabeltext.cc \
dnsname.cc dnsname.hh \
dnsparser.hh \
+ dnsrulactions.hh \
dnswriter.cc dnswriter.hh \
dolog.hh \
iputils.cc iputils.hh \
../dolog.hh ../iputils.cc ../iputils.hh ../misc.cc ../misc.hh ../namespaces.hh \
../pdnsexception.hh ../qtype.cc ../qtype.hh ../sholder.hh ../sodcrypto.cc ../sodcrypto.hh ../sstuff.hh .
+ln -fs ../README-dnsdist.md README.md
+
ln -fs ../dnsdistconf.lua .
mkdir -p pdns/ext/luawrapper/include
ln -sf ../../../../../ext/luawrapper/include/LuaContext.hpp pdns/ext/luawrapper/include
--- /dev/null
+#include "dnsdist.hh"
+#include "dnsname.hh"
+
+class NetmaskGroupRule : public DNSRule
+{
+public:
+ NetmaskGroupRule(const NetmaskGroup& nmg) : d_nmg(nmg)
+ {
+
+ }
+ bool matches(const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh) const override
+ {
+ return d_nmg.match(remote);
+ }
+
+ string toString() const override
+ {
+ return d_nmg.toString();
+ }
+private:
+ NetmaskGroup d_nmg;
+};
+
+class SuffixMatchNodeRule : public DNSRule
+{
+public:
+ SuffixMatchNodeRule(const SuffixMatchNode& smn) : d_smn(smn)
+ {
+ }
+ bool matches(const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh) const override
+ {
+ return d_smn.check(qname);
+ }
+ string toString() const override
+ {
+ return d_smn.toString();
+ }
+private:
+ SuffixMatchNode d_smn;
+};
+
+class QTypeRule : public DNSRule
+{
+public:
+ QTypeRule(uint16_t qtype) : d_qtype(qtype)
+ {
+ }
+ bool matches(const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh) const override
+ {
+ return d_qtype == qtype;
+ }
+ string toString() const override
+ {
+ QType qt(d_qtype);
+ return "qtype=="+qt.getName();
+ }
+private:
+ uint16_t d_qtype;
+};
+
+class DropAction : public DNSAction
+{
+public:
+ DNSAction::Action operator()(const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh, string* ruleresult) const override
+ {
+ return Action::Drop;
+ }
+ string toString() const override
+ {
+ return "drop";
+ }
+};
+
+class QPSAction : public DNSAction
+{
+public:
+ QPSAction(int limit) : d_qps(limit, limit)
+ {}
+ DNSAction::Action operator()(const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh, string* ruleresult) const override
+ {
+ if(d_qps.check())
+ return Action::Allow;
+ else
+ return Action::Drop;
+ }
+ string toString() const override
+ {
+ return "qps limit to "+std::to_string(d_qps.getRate());
+ }
+private:
+ QPSLimiter d_qps;
+};
+
+class PoolAction : public DNSAction
+{
+public:
+ PoolAction(const std::string& pool) : d_pool(pool) {}
+ DNSAction::Action operator()(const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh, string* ruleresult) const override
+ {
+ *ruleresult=d_pool;
+ return Action::Pool;
+ }
+ string toString() const override
+ {
+ return "to pool "+d_pool;
+ }
+
+private:
+ string d_pool;
+};
+
+class RCodeAction : public DNSAction
+{
+public:
+ RCodeAction(int rcode) : d_rcode(rcode) {}
+ DNSAction::Action operator()(const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh, string* ruleresult) const override
+ {
+ dh->rcode = d_rcode;
+ dh->qr = true; // for good measure
+ return Action::HeaderModify;
+ }
+ string toString() const override
+ {
+ return "set rcode "+std::to_string(d_rcode);
+ }
+
+private:
+ int d_rcode;
+};
+
+class TCAction : public DNSAction
+{
+public:
+ DNSAction::Action operator()(const ComboAddress& remote, const DNSName& qname, uint16_t qtype, dnsheader* dh, string* ruleresult) const override
+ {
+ dh->tc = true;
+ dh->qr = true; // for good measure
+ return Action::HeaderModify;
+ }
+ string toString() const override
+ {
+ return "tc=1 answer";
+ }
+};
}
};
+ struct addressOnlyEqual: public std::binary_function<ComboAddress, ComboAddress, bool>
+ {
+ bool operator()(const ComboAddress& a, const ComboAddress& b) const
+ {
+ if(a.sin4.sin_family != b.sin4.sin_family)
+ return false;
+ if(a.sin4.sin_family == AF_INET)
+ return a.sin4.sin_addr.s_addr == b.sin4.sin_addr.s_addr;
+ else
+ return !memcmp(&a.sin6.sin6_addr.s6_addr, &b.sin6.sin6_addr.s6_addr, 16);
+ }
+ };
+
+
socklen_t getSocklen() const
{
if(sin4.sin_family == AF_INET)