From: Remi Gacogne Date: Thu, 4 Aug 2016 10:37:47 +0000 (+0200) Subject: dnsdist: Add RCodeRule(), Allow, Delay and Drop response actions X-Git-Tag: rec-4.0.2~17^2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=788c3243ea34831d8e4f9e840526ec5d223d3fca;p=thirdparty%2Fpdns.git dnsdist: Add RCodeRule(), Allow, Delay and Drop response actions --- diff --git a/pdns/README-dnsdist.md b/pdns/README-dnsdist.md index 9afdd60b74..4a740b7598 100644 --- a/pdns/README-dnsdist.md +++ b/pdns/README-dnsdist.md @@ -333,6 +333,7 @@ Rules have selectors and actions. Current selectors are: * QType (QTypeRule) * RegexRule on query name * RE2Rule on query name (optional) + * Response code * Packet requests DNSSEC processing * Query received over UDP or TCP * Opcode (OpcodeRule) @@ -363,6 +364,9 @@ Current actions are: Current response actions are: + * Allow (AllowResponseAction) + * Delay a response by n milliseconds (DelayResponseAction), over UDP only + * Drop (DropResponseAction) * Log response content to a remote server (RemoteLogResponseAction) Rules can be added via: @@ -398,6 +402,7 @@ A DNS rule can be: * a QNameLabelsCountRule * a QNameWireLengthRule * a QTypeRule + * a RCodeRule * a RegexRule * a RE2Rule * a RecordsCountRule @@ -1230,6 +1235,7 @@ instantiate a server with additional parameters * `QNameLabelsCountRule(min, max)`: matches if the qname has less than `min` or more than `max` labels * `QNameWireLengthRule(min, max)`: matches if the qname's length on the wire is less than `min` or more than `max` bytes * `QTypeRule(qtype)`: matches queries with the specified qtype + * `RCodeRule(rcode)`: matches queries or responses the specified rcode * `RegexRule(regex)`: matches the query name against the supplied regex * `RecordsCountRule(section, minCount, maxCount)`: matches if there is at least `minCount` and at most `maxCount` records in the `section` section * `RecordsTypeCountRule(section, type, minCount, maxCount)`: matches if there is at least `minCount` and at most `maxCount` records of type `type` in the `section` section @@ -1254,9 +1260,12 @@ instantiate a server with additional parameters * `topRule()`: move the last rule to the first position * Built-in Actions for Rules: * `AllowAction()`: let these packets go through + * `AllowResponseAction()`: let these packets go through * `DelayAction(milliseconds)`: delay the response by the specified amount of milliseconds (UDP-only) + * `DelayResponseAction(milliseconds)`: delay the response by the specified amount of milliseconds (UDP-only) * `DisableValidationAction()`: set the CD bit in the question, let it go through * `DropAction()`: drop these packets + * `DropResponseAction()`: drop these packets * `LogAction([filename], [binary], [append], [buffered])`: Log a line for each query, to the specified file if any, to the console (require verbose) otherwise. When logging to a file, the `binary` optional parameter specifies whether we log in binary form (default) or in textual form, the `append` optional parameter specifies whether we open the file for appending or truncate each time (default), and the `buffered` optional parameter specifies whether writes to the file are buffered (default) or not. * `NoRecurseAction()`: strip RD bit from the question, let it go through * `PoolAction(poolname)`: set the packet into the specified pool diff --git a/pdns/dnsdist-console.cc b/pdns/dnsdist-console.cc index 619f3790c9..731ea810e0 100644 --- a/pdns/dnsdist-console.cc +++ b/pdns/dnsdist-console.cc @@ -238,6 +238,7 @@ const std::vector g_consoleKeywords{ { "addQPSPoolRule", true, "x, limit, pool", "like `addPoolRule`, but only select at most 'limit' queries/s for this pool, letting the subsequent rules apply otherwise" }, { "addResponseAction", true, "DNS rule, DNS response action", "add a response rule" }, { "AllowAction", true, "", "let these packets go through" }, + { "AllowResponseAction", true, "", "let these packets go through" }, { "AllRule", true, "", "matches all traffic" }, { "AndRule", true, "list of DNS rules", "matches if all sub-rules matches" }, { "benchRule", true, "DNS Rule [, iterations [, suffix]]", "bench the specified DNS rule" }, @@ -246,9 +247,11 @@ const std::vector g_consoleKeywords{ { "clearDynBlocks", true, "", "clear all dynamic blocks" }, { "clearRules", true, "", "remove all current rules" }, { "DelayAction", true, "milliseconds", "delay the response by the specified amount of milliseconds (UDP-only)" }, + { "DelayResponseAction", true, "milliseconds", "delay the response by the specified amount of milliseconds (UDP-only)" }, { "delta", true, "", "shows all commands entered that changed the configuration" }, { "DisableValidationAction", true, "", "set the CD bit in the question, let it go through" }, { "DropAction", true, "", "drop these packets" }, + { "DropResponseAction", true, "", "drop these packets" }, { "dumpStats", true, "", "print all statistics we gather" }, { "exceedNXDOMAINs", true, "rate, seconds", "get set of addresses that exceed `rate` NXDOMAIN/s over `seconds` seconds" }, { "exceedQRate", true, "rate, seconds", "get set of address that exceed `rate` queries/s over `seconds` seconds" }, @@ -291,6 +294,7 @@ const std::vector g_consoleKeywords{ { "QNameLabelsCountRule", true, "min, max", "matches if the qname has less than `min` or more than `max` labels" }, { "QNameWireLengthRule", true, "min, max", "matches if the qname's length on the wire is less than `min` or more than `max` bytes" }, { "QTypeRule", true, "qtype", "matches queries with the specified qtype" }, + { "RCodeRule", true, "rcode", "matches responses with the specified rcode" }, { "setACL", true, "{netmask, netmask}", "replace the ACL set with these netmasks. Use `setACL({})` to reset the list, meaning no one can use us" }, { "setDNSSECPool", true, "pool name", "move queries requesting DNSSEC processing to this pool" }, { "setECSOverride", true, "bool", "whether to override an existing EDNS Client Subnet value in the query" }, diff --git a/pdns/dnsdist-lua.cc b/pdns/dnsdist-lua.cc index bd997e197f..75aa60ac30 100644 --- a/pdns/dnsdist-lua.cc +++ b/pdns/dnsdist-lua.cc @@ -141,8 +141,11 @@ vector> setupLua(bool client, const std::string& confi ); g_lua.writeVariable("DNSResponseAction", std::unordered_map{ - {"None",(int)DNSResponseAction::Action::None}} - ); + {"Allow", (int)DNSResponseAction::Action::Allow }, + {"Delay", (int)DNSResponseAction::Action::Delay }, + {"HeaderModify", (int)DNSResponseAction::Action::HeaderModify }, + {"None", (int)DNSResponseAction::Action::None } + }); g_lua.writeVariable("DNSClass", std::unordered_map{ {"IN", QClass::IN }, @@ -903,6 +906,10 @@ vector> setupLua(bool client, const std::string& confi return std::shared_ptr(new QNameWireLengthRule(min, max)); }); + g_lua.writeFunction("RCodeRule", [](int rcode) { + return std::shared_ptr(new RCodeRule(rcode)); + }); + g_lua.writeFunction("addAction", [](luadnsrule_t var, std::shared_ptr ea) { setLuaSideEffect(); diff --git a/pdns/dnsdist-lua2.cc b/pdns/dnsdist-lua2.cc index 12066df03a..801a9e4f8e 100644 --- a/pdns/dnsdist-lua2.cc +++ b/pdns/dnsdist-lua2.cc @@ -667,6 +667,18 @@ void moreLua(bool client) g_lua.writeFunction("setVerboseHealthChecks", [](bool verbose) { g_verboseHealthChecks=verbose; }); g_lua.writeFunction("setStaleCacheEntriesTTL", [](uint32_t ttl) { g_staleCacheEntriesTTL = ttl; }); + g_lua.writeFunction("DropResponseAction", []() { + return std::shared_ptr(new DropResponseAction); + }); + + g_lua.writeFunction("AllowResponseAction", []() { + return std::shared_ptr(new AllowResponseAction); + }); + + g_lua.writeFunction("DelayResponseAction", [](int msec) { + return std::shared_ptr(new DelayResponseAction(msec)); + }); + g_lua.writeFunction("RemoteLogAction", [](std::shared_ptr logger) { #ifdef HAVE_PROTOBUF return std::shared_ptr(new RemoteLogAction(logger)); diff --git a/pdns/dnsdist-tcp.cc b/pdns/dnsdist-tcp.cc index 35c96d6712..c76cb1e196 100644 --- a/pdns/dnsdist-tcp.cc +++ b/pdns/dnsdist-tcp.cc @@ -435,7 +435,7 @@ void* tcpClientThread(int pipefd) #ifdef HAVE_PROTOBUF dr.uniqueId = dq.uniqueId; #endif - if (!processResponse(localRespRulactions, dr)) { + if (!processResponse(localRespRulactions, dr, &delayMsec)) { break; } diff --git a/pdns/dnsdist.cc b/pdns/dnsdist.cc index dbefaa8599..6a104d83a8 100644 --- a/pdns/dnsdist.cc +++ b/pdns/dnsdist.cc @@ -404,8 +404,8 @@ void* responderThread(std::shared_ptr state) #ifdef HAVE_PROTOBUF dr.uniqueId = ids->uniqueId; #endif - if (!processResponse(localRespRulactions, dr)) { - break; + if (!processResponse(localRespRulactions, dr, &ids->delayMsec)) { + continue; } #ifdef HAVE_DNSCRYPT @@ -821,14 +821,31 @@ bool processQuery(LocalStateHolder >& localDynNMGBlock, return true; } -bool processResponse(LocalStateHolder, std::shared_ptr > > >& localRespRulactions, DNSResponse& dr) +bool processResponse(LocalStateHolder, std::shared_ptr > > >& localRespRulactions, DNSResponse& dr, int* delayMsec) { + DNSResponseAction::Action action=DNSResponseAction::Action::None; std::string ruleresult; for(const auto& lr : *localRespRulactions) { if(lr.first->matches(&dr)) { lr.first->d_matches++; - /* for now we only support actions returning None */ - (*lr.second)(&dr, &ruleresult); + action=(*lr.second)(&dr, &ruleresult); + switch(action) { + case DNSResponseAction::Action::Allow: + return true; + break; + case DNSResponseAction::Action::Drop: + return false; + break; + case DNSResponseAction::Action::HeaderModify: + return true; + break; + /* non-terminal actions follow */ + case DNSResponseAction::Action::Delay: + *delayMsec = static_cast(pdns_stou(ruleresult)); // sorry + break; + case DNSResponseAction::Action::None: + break; + } } } diff --git a/pdns/dnsdist.hh b/pdns/dnsdist.hh index 6e9538c1f5..9de599632d 100644 --- a/pdns/dnsdist.hh +++ b/pdns/dnsdist.hh @@ -449,7 +449,7 @@ public: class DNSResponseAction { public: - enum class Action { None }; + enum class Action { Allow, Delay, Drop, HeaderModify, None }; virtual Action operator()(DNSResponse*, string* ruleresult) const =0; virtual string toString() const = 0; }; @@ -669,7 +669,7 @@ void resetLuaSideEffect(); // reset to indeterminate state bool responseContentMatches(const char* response, const uint16_t responseLen, const DNSName& qname, const uint16_t qtype, const uint16_t qclass, const ComboAddress& remote); bool processQuery(LocalStateHolder >& localDynBlockNMG, LocalStateHolder >& localDynBlockSMT, LocalStateHolder, std::shared_ptr > > >& localRulactions, blockfilter_t blockFilter, DNSQuestion& dq, string& poolname, int* delayMsec, const struct timespec& now); -bool processResponse(LocalStateHolder, std::shared_ptr > > >& localRespRulactions, DNSResponse& dr); +bool processResponse(LocalStateHolder, std::shared_ptr > > >& localRespRulactions, DNSResponse& dr, int* delayMsec); bool fixUpResponse(char** response, uint16_t* responseLen, size_t* responseSize, const DNSName& qname, uint16_t origFlags, bool ednsAdded, bool ecsAdded, std::vector& rewrittenResponse, uint16_t addRoom); void restoreFlags(struct dnsheader* dh, uint16_t origFlags); diff --git a/pdns/dnsrulactions.hh b/pdns/dnsrulactions.hh index 28575ea510..c7a80870d9 100644 --- a/pdns/dnsrulactions.hh +++ b/pdns/dnsrulactions.hh @@ -528,6 +528,25 @@ private: size_t d_max; }; +class RCodeRule : public DNSRule +{ +public: + RCodeRule(int rcode) : d_rcode(rcode) + { + } + bool matches(const DNSQuestion* dq) const override + { + return d_rcode == dq->dh->rcode; + } + string toString() const override + { + return "rcode=="+RCode::to_s(d_rcode); + } +private: + int d_rcode; +}; + + class DropAction : public DNSAction { public: @@ -996,3 +1015,47 @@ public: private: std::shared_ptr d_logger; }; + +class DropResponseAction : public DNSResponseAction +{ +public: + DNSResponseAction::Action operator()(DNSResponse* dr, string* ruleresult) const override + { + return Action::Drop; + } + string toString() const override + { + return "drop"; + } +}; + +class AllowResponseAction : public DNSResponseAction +{ +public: + DNSResponseAction::Action operator()(DNSResponse* dr, string* ruleresult) const override + { + return Action::Allow; + } + string toString() const override + { + return "allow"; + } +}; + +class DelayResponseAction : public DNSResponseAction +{ +public: + DelayResponseAction(int msec) : d_msec(msec) + {} + DNSResponseAction::Action operator()(DNSResponse* dr, string* ruleresult) const override + { + *ruleresult=std::to_string(d_msec); + return Action::Delay; + } + string toString() const override + { + return "delay by "+std::to_string(d_msec)+ " msec"; + } +private: + int d_msec; +}; diff --git a/regression-tests.dnsdist/test_Responses.py b/regression-tests.dnsdist/test_Responses.py new file mode 100644 index 0000000000..096b09d7ed --- /dev/null +++ b/regression-tests.dnsdist/test_Responses.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python +from datetime import datetime, timedelta +import time +import dns +from dnsdisttests import DNSDistTest + +class TestResponseRuleNXDelayed(DNSDistTest): + + _config_template = """ + newServer{address="127.0.0.1:%s"} + addResponseAction(RCodeRule(dnsdist.NXDOMAIN), DelayResponseAction(1000)) + """ + + def testNXDelayed(self): + """ + Responses: Delayed on NXDomain + + Send an A query to "delayed.responses.tests.powerdns.com.", + check that the response delay is longer than 1000 ms + for a NXDomain response over UDP, shorter for a NoError one. + """ + name = 'delayed.responses.tests.powerdns.com.' + query = dns.message.make_query(name, 'A', 'IN') + response = dns.message.make_response(query) + + # NX over UDP + response.set_rcode(dns.rcode.NXDOMAIN) + begin = datetime.now() + (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response) + end = datetime.now() + receivedQuery.id = query.id + self.assertEquals(query, receivedQuery) + self.assertEquals(response, receivedResponse) + self.assertTrue((end - begin) > timedelta(0, 1)) + + # NoError over UDP + response.set_rcode(dns.rcode.NOERROR) + begin = datetime.now() + (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response) + end = datetime.now() + receivedQuery.id = query.id + self.assertEquals(query, receivedQuery) + self.assertEquals(response, receivedResponse) + self.assertTrue((end - begin) < timedelta(0, 1)) + + # NX over TCP + response.set_rcode(dns.rcode.NXDOMAIN) + begin = datetime.now() + (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response) + end = datetime.now() + receivedQuery.id = query.id + self.assertEquals(query, receivedQuery) + self.assertEquals(response, receivedResponse) + self.assertTrue((end - begin) < timedelta(0, 1)) + +class TestResponseRuleQNameDropped(DNSDistTest): + + _config_template = """ + newServer{address="127.0.0.1:%s"} + addResponseAction("drop.responses.tests.powerdns.com.", DropResponseAction()) + """ + + def testDropped(self): + """ + Responses: Dropped on QName + + Send an A query to "drop.responses.tests.powerdns.com.", + check that the response (not the query) is dropped. + """ + name = 'drop.responses.tests.powerdns.com.' + query = dns.message.make_query(name, 'A', 'IN') + response = dns.message.make_response(query) + + (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response) + receivedQuery.id = query.id + self.assertEquals(query, receivedQuery) + self.assertEquals(receivedResponse, None) + + (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response) + receivedQuery.id = query.id + self.assertEquals(query, receivedQuery) + self.assertEquals(receivedResponse, None) + + def testNotDropped(self): + """ + Responses: NOT Dropped on QName + + Send an A query to "dontdrop.responses.tests.powerdns.com.", + check that the response is not dropped. + """ + name = 'dontdrop.responses.tests.powerdns.com.' + query = dns.message.make_query(name, 'A', 'IN') + response = dns.message.make_response(query) + + (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response) + receivedQuery.id = query.id + self.assertEquals(query, receivedQuery) + self.assertEquals(response, receivedResponse) + + (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response) + receivedQuery.id = query.id + self.assertEquals(query, receivedQuery) + self.assertEquals(response, receivedResponse) + +class TestResponseRuleQNameAllowed(DNSDistTest): + + _config_template = """ + newServer{address="127.0.0.1:%s"} + addResponseAction("allow.responses.tests.powerdns.com.", AllowResponseAction()) + addResponseAction(AllRule(), DropResponseAction()) + """ + + def testAllowed(self): + """ + Responses: Allowed on QName + + Send an A query to "allow.responses.tests.powerdns.com.", + check that the response is allowed. + """ + name = 'allow.responses.tests.powerdns.com.' + query = dns.message.make_query(name, 'A', 'IN') + response = dns.message.make_response(query) + + (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response) + receivedQuery.id = query.id + self.assertEquals(query, receivedQuery) + self.assertEquals(response, receivedResponse) + + (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response) + receivedQuery.id = query.id + self.assertEquals(query, receivedQuery) + self.assertEquals(response, receivedResponse) + + def testNotAllowed(self): + """ + Responses: Not allowed on QName + + Send an A query to "dontallow.responses.tests.powerdns.com.", + check that the response is dropped. + """ + name = 'dontallow.responses.tests.powerdns.com.' + query = dns.message.make_query(name, 'A', 'IN') + response = dns.message.make_response(query) + + (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response) + receivedQuery.id = query.id + self.assertEquals(query, receivedQuery) + self.assertEquals(receivedResponse, None) + + (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response) + receivedQuery.id = query.id + self.assertEquals(query, receivedQuery) + self.assertEquals(receivedResponse, None)