From: Remi Gacogne Date: Thu, 8 Jun 2017 11:01:17 +0000 (+0200) Subject: dnsdist: Make a `truncate` action available to DynBlock and Lua X-Git-Tag: rec-4.1.0-alpha1~69^2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=c4f5aeff6c34acc258205d8aecbe7a8147e9c338;p=thirdparty%2Fpdns.git dnsdist: Make a `truncate` action available to DynBlock and Lua --- diff --git a/pdns/README-dnsdist.md b/pdns/README-dnsdist.md index c5a79312d6..1a8c0a6adf 100644 --- a/pdns/README-dnsdist.md +++ b/pdns/README-dnsdist.md @@ -590,6 +590,7 @@ Valid return values for `LuaAction` functions are: * DNSAction.Pool: use the specified pool to forward this query * DNSAction.Refused: return a response with a Refused rcode * DNSAction.Spoof: spoof the response using the supplied IPv4 (A), IPv6 (AAAA) or string (CNAME) value + * DNSAction.Truncate: return a response with TC=1 The same feature exists to hand off some responses for Lua inspection, using `addLuaResponseAction(x, func)`. @@ -1531,7 +1532,7 @@ instantiate a server with additional parameters * `clearDynBlocks()`: clear all dynamic blocks * `showDynBlocks()`: show dynamic blocks in force * `addDynBlocks(addresses, message[, seconds[, action]])`: block the set of addresses with message `msg`, for `seconds` seconds (10 by default), applying `action` (default to the one set with `setDynBlocksAction()`) - * `setDynBlocksAction(DNSAction)`: set which action is performed when a query is blocked. Only DNSAction.Drop (the default) and DNSAction.Refused are supported + * `setDynBlocksAction(DNSAction)`: set which action is performed when a query is blocked. Only DNSAction.Drop (the default), DNSAction.Refused and DNSAction.Truncate are supported * `addBPFFilterDynBlocks(addresses, DynBPFFilter[, seconds])`: block the set of addresses using the supplied BPF Filter, for `seconds` seconds (10 by default) * `exceedServFails(rate, seconds)`: get set of addresses that exceed `rate` servfails/s over `seconds` seconds * `exceedNXDOMAINs(rate, seconds)`: get set of addresses that exceed `rate` NXDOMAIN/s over `seconds` seconds diff --git a/pdns/dnsdist-lua.cc b/pdns/dnsdist-lua.cc index a56181b878..14279d4860 100644 --- a/pdns/dnsdist-lua.cc +++ b/pdns/dnsdist-lua.cc @@ -209,8 +209,9 @@ vector> setupLua(bool client, const std::string& confi {"HeaderModify", (int)DNSAction::Action::HeaderModify}, {"Pool", (int)DNSAction::Action::Pool}, {"None",(int)DNSAction::Action::None}, - {"Delay", (int)DNSAction::Action::Delay}} - ); + {"Delay", (int)DNSAction::Action::Delay}, + {"Truncate", (int)DNSAction::Action::Truncate} + }); g_lua.writeVariable("DNSResponseAction", std::unordered_map{ {"Allow", (int)DNSResponseAction::Action::Allow }, diff --git a/pdns/dnsdist-lua2.cc b/pdns/dnsdist-lua2.cc index 3f31e2bee5..c25a8780a5 100644 --- a/pdns/dnsdist-lua2.cc +++ b/pdns/dnsdist-lua2.cc @@ -317,12 +317,12 @@ void moreLua(bool client) g_lua.writeFunction("setDynBlocksAction", [](DNSAction::Action action) { if (!g_configurationDone) { - if (action == DNSAction::Action::Drop || action == DNSAction::Action::Refused) { + if (action == DNSAction::Action::Drop || action == DNSAction::Action::Refused || action == DNSAction::Action::Truncate) { g_dynBlockAction = action; } else { - errlog("Dynamic blocks action can only be Drop or Refused!"); - g_outputBuffer="Dynamic blocks action can only be Drop or Refused!\n"; + errlog("Dynamic blocks action can only be Drop, Refused or Truncate!"); + g_outputBuffer="Dynamic blocks action can only be Drop, Refused or Truncate!\n"; } } else { g_outputBuffer="Dynamic blocks action cannot be altered at runtime!\n"; diff --git a/pdns/dnsdist.cc b/pdns/dnsdist.cc index 592eae28e7..9a365662c2 100644 --- a/pdns/dnsdist.cc +++ b/pdns/dnsdist.cc @@ -863,6 +863,12 @@ bool processQuery(LocalStateHolder >& localDynNMGBlock, dq.dh->qr=true; return true; } + else if (action == DNSAction::Action::Truncate && !dq.tcp) { + vinfolog("Query from %s truncated because of dynamic block", dq.remote->toStringWithPort()); + dq.dh->tc = true; + dq.dh->qr = true; + return true; + } else { vinfolog("Query from %s dropped because of dynamic block", dq.remote->toStringWithPort()); return false; @@ -884,6 +890,12 @@ bool processQuery(LocalStateHolder >& localDynNMGBlock, dq.dh->qr=true; return true; } + else if (action == DNSAction::Action::Truncate && !dq.tcp) { + vinfolog("Query from %s for %s truncated because of dynamic block", dq.remote->toStringWithPort(), dq.qname->toString()); + dq.dh->tc = true; + dq.dh->qr = true; + return true; + } else { vinfolog("Query from %s for %s dropped because of dynamic block", dq.remote->toStringWithPort(), dq.qname->toString()); return false; @@ -931,6 +943,11 @@ bool processQuery(LocalStateHolder >& localDynNMGBlock, spoofResponseFromString(dq, ruleresult); return true; break; + case DNSAction::Action::Truncate: + dq.dh->tc = true; + dq.dh->qr = true; + return true; + break; case DNSAction::Action::HeaderModify: return true; break; diff --git a/pdns/dnsdist.hh b/pdns/dnsdist.hh index a65060f1f5..1b05cb47b6 100644 --- a/pdns/dnsdist.hh +++ b/pdns/dnsdist.hh @@ -92,7 +92,7 @@ struct DNSResponse : DNSQuestion class DNSAction { public: - enum class Action { Drop, Nxdomain, Refused, Spoof, Allow, HeaderModify, Pool, Delay, None}; + enum class Action { Drop, Nxdomain, Refused, Spoof, Allow, HeaderModify, Pool, Delay, Truncate, None}; virtual Action operator()(DNSQuestion*, string* ruleresult) const =0; virtual string toString() const = 0; virtual std::unordered_map getStats() const diff --git a/pdns/dnsrulactions.hh b/pdns/dnsrulactions.hh index a9cf5e50de..a7b2738fdc 100644 --- a/pdns/dnsrulactions.hh +++ b/pdns/dnsrulactions.hh @@ -779,9 +779,7 @@ class TCAction : public DNSAction public: DNSAction::Action operator()(DNSQuestion* dq, string* ruleresult) const override { - dq->dh->tc = true; - dq->dh->qr = true; // for good measure - return Action::HeaderModify; + return Action::Truncate; } string toString() const override { diff --git a/regression-tests.dnsdist/test_Advanced.py b/regression-tests.dnsdist/test_Advanced.py index 00ec12b36e..7864d8d3c0 100644 --- a/regression-tests.dnsdist/test_Advanced.py +++ b/regression-tests.dnsdist/test_Advanced.py @@ -1236,8 +1236,6 @@ class TestAdvancedLuaDO(DNSDistTest): (_, receivedResponse) = self.sendUDPQuery(queryWithDO, response=None, useQueue=False) self.assertTrue(receivedResponse) doResponse.id = receivedResponse.id - print(doResponse) - print(receivedResponse) self.assertEquals(receivedResponse, doResponse) (_, receivedResponse) = self.sendTCPQuery(queryWithDO, response=None, useQueue=False) @@ -1245,6 +1243,85 @@ class TestAdvancedLuaDO(DNSDistTest): doResponse.id = receivedResponse.id self.assertEquals(receivedResponse, doResponse) +class TestAdvancedLuaRefused(DNSDistTest): + + _config_template = """ + function refuse(dq) + return DNSAction.Refused, "" + end + addLuaAction(AllRule(), refuse) + newServer{address="127.0.0.1:%s"} + """ + + def testRefusedViaLua(self): + """ + Advanced: Refused via Lua + """ + name = 'refused.advanced.tests.powerdns.com.' + query = dns.message.make_query(name, 'A', 'IN') + response = dns.message.make_response(query) + rrset = dns.rrset.from_text(name, + 3600, + dns.rdataclass.IN, + dns.rdatatype.AAAA, + '::1') + response.answer.append(rrset) + refusedResponse = dns.message.make_response(query) + refusedResponse.set_rcode(dns.rcode.REFUSED) + + (_, receivedResponse) = self.sendUDPQuery(query, response=None, useQueue=False) + self.assertTrue(receivedResponse) + refusedResponse.id = receivedResponse.id + self.assertEquals(receivedResponse, refusedResponse) + + (_, receivedResponse) = self.sendTCPQuery(query, response=None, useQueue=False) + self.assertTrue(receivedResponse) + refusedResponse.id = receivedResponse.id + self.assertEquals(receivedResponse, refusedResponse) + +class TestAdvancedLuaTruncated(DNSDistTest): + + _config_template = """ + function trunc(dq) + if not dq.tcp then + return DNSAction.Truncate, "" + end + return DNSAction.None, "" + end + addLuaAction(AllRule(), trunc) + newServer{address="127.0.0.1:%s"} + """ + + def testTCViaLua(self): + """ + Advanced: TC via Lua + """ + name = 'tc.advanced.tests.powerdns.com.' + query = dns.message.make_query(name, 'A', 'IN') + response = dns.message.make_response(query) + rrset = dns.rrset.from_text(name, + 3600, + dns.rdataclass.IN, + dns.rdatatype.AAAA, + '::1') + response.answer.append(rrset) + + truncatedResponse = dns.message.make_response(query) + truncatedResponse.flags |= dns.flags.TC + + (_, receivedResponse) = self.sendUDPQuery(query, response=None, useQueue=False) + self.assertTrue(receivedResponse) + truncatedResponse.id = receivedResponse.id + self.assertEquals(receivedResponse, truncatedResponse) + + # no truncation over TCP + (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response) + self.assertTrue(receivedQuery) + self.assertTrue(receivedResponse) + receivedQuery.id = query.id + self.assertEquals(query, receivedQuery) + self.assertEquals(receivedResponse, response) + class TestStatNodeRespRingSince(DNSDistTest): _consoleKey = DNSDistTest.generateConsoleKey() diff --git a/regression-tests.dnsdist/test_DynBlocks.py b/regression-tests.dnsdist/test_DynBlocks.py index 548a9889c5..b0aea87c6e 100644 --- a/regression-tests.dnsdist/test_DynBlocks.py +++ b/regression-tests.dnsdist/test_DynBlocks.py @@ -314,6 +314,86 @@ class TestDynBlockQPSActionRefused(DNSDistTest): self.assertEquals(query, receivedQuery) self.assertEquals(response, receivedResponse) +class TestDynBlockQPSActionTruncated(DNSDistTest): + + _dynBlockQPS = 10 + _dynBlockPeriod = 2 + _dynBlockDuration = 5 + _config_params = ['_dynBlockQPS', '_dynBlockPeriod', '_dynBlockDuration', '_testServerPort'] + _config_template = """ + function maintenance() + addDynBlocks(exceedQRate(%d, %d), "Exceeded query rate", %d, DNSAction.Truncate) + end + setDynBlocksAction(DNSAction.Drop) + newServer{address="127.0.0.1:%s"} + """ + + def testDynBlocksQRate(self): + """ + Dyn Blocks: QRate truncated (action) + """ + name = 'qrateactiontruncated.dynblocks.tests.powerdns.com.' + query = dns.message.make_query(name, 'A', 'IN') + response = dns.message.make_response(query) + rrset = dns.rrset.from_text(name, + 60, + dns.rdataclass.IN, + dns.rdatatype.A, + '192.0.2.1') + response.answer.append(rrset) + truncatedResponse = dns.message.make_response(query) + truncatedResponse.flags |= dns.flags.TC + + allowed = 0 + sent = 0 + for _ in xrange((self._dynBlockQPS * self._dynBlockPeriod) + 1): + (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response) + sent = sent + 1 + if receivedQuery: + receivedQuery.id = query.id + self.assertEquals(query, receivedQuery) + self.assertEquals(receivedResponse, response) + allowed = allowed + 1 + else: + self.assertEquals(receivedResponse, truncatedResponse) + # the query has not reached the responder, + # let's clear the response queue + self.clearToResponderQueue() + + # we might be already blocked, but we should have been able to send + # at least self._dynBlockQPS queries + self.assertGreaterEqual(allowed, self._dynBlockQPS) + + if allowed == sent: + # wait for the maintenance function to run + time.sleep(2) + + # we should now be 'truncated' for up to self._dynBlockDuration + self._dynBlockPeriod + (_, receivedResponse) = self.sendUDPQuery(query, response=None, useQueue=False) + self.assertEquals(receivedResponse, truncatedResponse) + + # wait until we are not blocked anymore + time.sleep(self._dynBlockDuration + self._dynBlockPeriod) + + # this one should succeed + (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response) + receivedQuery.id = query.id + self.assertEquals(query, receivedQuery) + self.assertEquals(response, receivedResponse) + + allowed = 0 + sent = 0 + # again, over TCP this time, we should never get truncated! + for _ in xrange((self._dynBlockQPS * self._dynBlockPeriod) + 1): + (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response) + sent = sent + 1 + self.assertEquals(query, receivedQuery) + self.assertEquals(receivedResponse, response) + receivedQuery.id = query.id + allowed = allowed + 1 + + self.assertEquals(allowed, sent) + class TestDynBlockServFails(DNSDistTest): _dynBlockQPS = 10