From: Chris Hofstaedtler Date: Mon, 15 Jan 2018 21:40:09 +0000 (+0100) Subject: dnsdist: add rules for self-answered responses X-Git-Tag: dnsdist-1.3.0~127^2 X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=2d4783a8aff591b9ca9d27c4376c2a4df2a14efc;p=pdns dnsdist: add rules for self-answered responses --- diff --git a/pdns/dnsdist-console.cc b/pdns/dnsdist-console.cc index 04aa78422..1f267c0aa 100644 --- a/pdns/dnsdist-console.cc +++ b/pdns/dnsdist-console.cc @@ -292,6 +292,7 @@ const std::vector g_consoleKeywords{ { "addLuaResponseAction", true, "x, func [, {uuid=\"UUID\"}]", "where 'x' is all the combinations from `addAction`, and func is a function with the parameter `dr`, which returns an action to be taken on this response packet. Good for rare packets but where you want to do a lot of processing" }, { "addCacheHitResponseAction", true, "DNS rule, DNS response action [, {uuid=\"UUID\"}]", "add a cache hit response rule" }, { "addResponseAction", true, "DNS rule, DNS response action [, {uuid=\"UUID\"}]", "add a response rule" }, + { "addSelfAnsweredResponseAction", true, "DNS rule, DNS response action [, {uuid=\"UUID\"}]", "add a self-answered response rule" }, { "addTLSLocal", true, "addr, certFile, keyFile[,params]", "listen to incoming DNS over TLS queries on the specified address using the specified certificate and key. The last parameter is a table" }, { "AllowAction", true, "", "let these packets go through" }, { "AllowResponseAction", true, "", "let these packets go through" }, @@ -338,6 +339,7 @@ const std::vector g_consoleKeywords{ { "mvCacheHitResponseRule", true, "from, to", "move cache hit response rule 'from' to a position where it is in front of 'to'. 'to' can be one larger than the largest rule" }, { "mvResponseRule", true, "from, to", "move response rule 'from' to a position where it is in front of 'to'. 'to' can be one larger than the largest rule" }, { "mvRule", true, "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" }, + { "mvSelfAnsweredResponseRule", true, "from, to", "move self-answered response rule 'from' to a position where it is in front of 'to'. 'to' can be one larger than the largest rule" }, { "newDNSName", true, "name", "make a DNSName based on this .-terminated name" }, { "newPacketCache", true, "maxEntries[, maxTTL=86400, minTTL=0, temporaryFailureTTL=60, staleTTL=60, dontAge=false, numberOfShards=1, deferrableInsertLock=true]", "return a new Packet Cache" }, { "newQPSLimiter", true, "rate, burst", "configure a QPS limiter with that rate and that burst capacity" }, @@ -356,6 +358,7 @@ const std::vector g_consoleKeywords{ { "rmCacheHitResponseRule", true, "id", "remove cache hit response rule in position 'id', or whose uuid matches if 'id' is an UUID string" }, { "rmResponseRule", true, "id", "remove response rule in position 'id', or whose uuid matches if 'id' is an UUID string" }, { "rmRule", true, "id", "remove rule in position 'id', or whose uuid matches if 'id' is an UUID string" }, + { "rmSelfAnsweredResponseRule", true, "id", "remove self-answered response rule in position 'id', or whose uuid matches if 'id' is an UUID string" }, { "rmServer", true, "n", "remove server with index n" }, { "roundrobin", false, "", "Simple round robin over available servers" }, { "QNameLabelsCountRule", true, "min, max", "matches if the qname has less than `min` or more than `max` labels" }, @@ -409,6 +412,7 @@ const std::vector g_consoleKeywords{ { "showResponseLatency", true, "", "show a plot of the response time latency distribution" }, { "showResponseRules", true, "[showUUIDs]", "show all defined response rules, optionally with their UUIDs" }, { "showRules", true, "[showUUIDs]", "show all defined rules, optionally with their UUIDs" }, + { "showSelfAnsweredResponseRules", true, "[showUUIDs]", "show all defined self-answered response rules, optionally with their UUIDs" }, { "showServerPolicy", true, "", "show name of currently operational server selection policy" }, { "showServers", true, "", "output all servers" }, { "showTCPStats", true, "", "show some statistics regarding TCP" }, @@ -432,8 +436,10 @@ const std::vector g_consoleKeywords{ { "topClients", true, "n", "show top-`n` clients sending the most queries over length of ringbuffer" }, { "topQueries", true, "n[, labels]", "show top 'n' queries, as grouped when optionally cut down to 'labels' labels" }, { "topResponses", true, "n, kind[, labels]", "show top 'n' responses with RCODE=kind (0=NO Error, 2=ServFail, 3=ServFail), as grouped when optionally cut down to 'labels' labels" }, + { "topCacheHitResponseRule", true, "", "move the last cache hit response rule to the first position" }, { "topResponseRule", true, "", "move the last response rule to the first position" }, { "topRule", true, "", "move the last rule to the first position" }, + { "topSelfAnsweredResponseRule", true, "", "move the last self-answered response rule to the first position" }, { "topSlow", true, "[top][, limit][, labels]", "show `top` queries slower than `limit` milliseconds, grouped by last `labels` labels" }, { "truncateTC", true, "bool", "if set (defaults to no starting with dnsdist 1.2.0) truncate TC=1 answers so they are actually empty. Fixes an issue for PowerDNS Authoritative Server 2.9.22. Note: turning this on breaks compatibility with RFC 6891." }, { "unregisterDynBPFFilter", true, "DynBPFFilter", "unregister this dynamic BPF filter" }, diff --git a/pdns/dnsdist-lua-actions.cc b/pdns/dnsdist-lua-actions.cc index 9b86a8997..351d407fb 100644 --- a/pdns/dnsdist-lua-actions.cc +++ b/pdns/dnsdist-lua-actions.cc @@ -864,6 +864,14 @@ void setupLuaActions() addAction(&g_cachehitresprulactions, var, boost::get >(era), params); }); + g_lua.writeFunction("addSelfAnsweredResponseAction", [](luadnsrule_t var, boost::variant, std::shared_ptr> era, boost::optional params) { + if (era.type() != typeid(std::shared_ptr)) { + throw std::runtime_error("addSelfAnsweredResponseAction() can only be called with response-related actions, not query-related ones. Are you looking for addAction()?"); + } + + addAction(&g_selfansweredresprulactions, var, boost::get >(era), params); + }); + g_lua.registerFunction("printStats", [](const DNSAction& ta) { setLuaNoSideEffect(); auto stats = ta.getStats(); diff --git a/pdns/dnsdist-lua-rules.cc b/pdns/dnsdist-lua-rules.cc index 4e2e53c8c..a6ffe6455 100644 --- a/pdns/dnsdist-lua-rules.cc +++ b/pdns/dnsdist-lua-rules.cc @@ -1034,6 +1034,22 @@ void setupLuaRules() mvRule(&g_cachehitresprulactions, from, to); }); + g_lua.writeFunction("showSelfAnsweredResponseRules", [](boost::optional showUUIDs) { + showRules(&g_selfansweredresprulactions, showUUIDs); + }); + + g_lua.writeFunction("rmSelfAnsweredResponseRule", [](boost::variant id) { + rmRule(&g_selfansweredresprulactions, id); + }); + + g_lua.writeFunction("topSelfAnsweredResponseRule", []() { + topRule(&g_selfansweredresprulactions); + }); + + g_lua.writeFunction("mvSelfAnsweredResponseRule", [](unsigned int from, unsigned int to) { + mvRule(&g_selfansweredresprulactions, from, to); + }); + g_lua.writeFunction("rmRule", [](boost::variant id) { rmRule(&g_rulactions, id); }); diff --git a/pdns/dnsdist-tcp.cc b/pdns/dnsdist-tcp.cc index f23b4c158..6fa56882d 100644 --- a/pdns/dnsdist-tcp.cc +++ b/pdns/dnsdist-tcp.cc @@ -361,6 +361,17 @@ void* tcpClientThread(int pipefd) if(dq.dh->qr) { // something turned it into a response restoreFlags(dh, origFlags); + + DNSResponse dr(dq.qname, dq.qtype, dq.qclass, dq.local, dq.remote, reinterpret_cast(query), dq.size, dq.len, true, &queryRealTime); +#ifdef HAVE_PROTOBUF + dr.uniqueId = dq.uniqueId; +#endif + dr.qTag = dq.qTag; + + if (!processResponse(holders.selfAnsweredRespRulactions, dr, &delayMsec)) { + goto drop; + } + #ifdef HAVE_DNSCRYPT if (!encryptResponse(query, &dq.len, dq.size, true, dnsCryptQuery, nullptr, nullptr)) { goto drop; @@ -428,6 +439,16 @@ void* tcpClientThread(int pipefd) dq.dh->rcode = RCode::ServFail; dq.dh->qr = true; + DNSResponse dr(dq.qname, dq.qtype, dq.qclass, dq.local, dq.remote, reinterpret_cast(query), dq.size, dq.len, false, &queryRealTime); +#ifdef HAVE_PROTOBUF + dr.uniqueId = dq.uniqueId; +#endif + dr.qTag = dq.qTag; + + if (!processResponse(holders.selfAnsweredRespRulactions, dr, &delayMsec)) { + goto drop; + } + #ifdef HAVE_DNSCRYPT if (!encryptResponse(query, &dq.len, dq.size, true, dnsCryptQuery, nullptr, nullptr)) { goto drop; diff --git a/pdns/dnsdist-web.cc b/pdns/dnsdist-web.cc index 6af354d56..b2bca4335 100644 --- a/pdns/dnsdist-web.cc +++ b/pdns/dnsdist-web.cc @@ -479,6 +479,7 @@ static void connectionThread(int sock, ComboAddress remote, string password, str auto responseRules = someResponseRulesToJson(&g_resprulactions); auto cacheHitResponseRules = someResponseRulesToJson(&g_cachehitresprulactions); + auto selfAnsweredResponseRules = someResponseRulesToJson(&g_selfansweredresprulactions); string acl; @@ -504,6 +505,7 @@ static void connectionThread(int sock, ComboAddress remote, string password, str { "rules", rules}, { "response-rules", responseRules}, { "cache-hit-response-rules", cacheHitResponseRules}, + { "self-answered-response-rules", selfAnsweredResponseRules}, { "acl", acl}, { "local", localaddresses} }; diff --git a/pdns/dnsdist.cc b/pdns/dnsdist.cc index 1ff98fd36..decd88d75 100644 --- a/pdns/dnsdist.cc +++ b/pdns/dnsdist.cc @@ -135,6 +135,7 @@ DNSDistSNMPAgent* g_snmpAgent{nullptr}; GlobalStateHolder > g_rulactions; GlobalStateHolder > g_resprulactions; GlobalStateHolder > g_cachehitresprulactions; +GlobalStateHolder > g_selfansweredresprulactions; Rings g_rings; QueryCount g_qcount; @@ -1236,6 +1237,16 @@ static void processUDPQuery(ClientState& cs, LocalHolders& holders, const struct char* response = query; uint16_t responseLen = dq.len; + DNSResponse dr(dq.qname, dq.qtype, dq.qclass, dq.local, dq.remote, reinterpret_cast(response), dq.size, responseLen, false, &realTime); +#ifdef HAVE_PROTOBUF + dr.uniqueId = dq.uniqueId; +#endif + dr.qTag = dq.qTag; + + if (!processResponse(holders.selfAnsweredRespRulactions, dr, &delayMsec)) { + return; + } + #ifdef HAVE_DNSCRYPT if (!encryptResponse(response, &responseLen, dq.size, false, dnsCryptQuery, nullptr, nullptr)) { return; @@ -1330,6 +1341,16 @@ static void processUDPQuery(ClientState& cs, LocalHolders& holders, const struct dq.dh->rcode = RCode::ServFail; dq.dh->qr = true; + DNSResponse dr(dq.qname, dq.qtype, dq.qclass, dq.local, dq.remote, reinterpret_cast(response), dq.size, responseLen, false, &realTime); +#ifdef HAVE_PROTOBUF + dr.uniqueId = dq.uniqueId; +#endif + dr.qTag = dq.qTag; + + if (!processResponse(holders.selfAnsweredRespRulactions, dr, &delayMsec)) { + return; + } + #ifdef HAVE_DNSCRYPT if (!encryptResponse(response, &responseLen, dq.size, false, dnsCryptQuery, nullptr, nullptr)) { return; diff --git a/pdns/dnsdist.hh b/pdns/dnsdist.hh index aff57f207..c7b68e154 100644 --- a/pdns/dnsdist.hh +++ b/pdns/dnsdist.hh @@ -749,6 +749,7 @@ extern GlobalStateHolder g_pools; extern GlobalStateHolder > g_rulactions; extern GlobalStateHolder > g_resprulactions; extern GlobalStateHolder > g_cachehitresprulactions; +extern GlobalStateHolder > g_selfansweredresprulactions; extern GlobalStateHolder g_ACL; extern ComboAddress g_serverControl; // not changed during runtime @@ -807,7 +808,7 @@ extern std::vector > g_dynBPFFilters; struct LocalHolders { - LocalHolders(): acl(g_ACL.getLocal()), policy(g_policy.getLocal()), rulactions(g_rulactions.getLocal()), cacheHitRespRulactions(g_cachehitresprulactions.getLocal()), servers(g_dstates.getLocal()), dynNMGBlock(g_dynblockNMG.getLocal()), dynSMTBlock(g_dynblockSMT.getLocal()), pools(g_pools.getLocal()) + LocalHolders(): acl(g_ACL.getLocal()), policy(g_policy.getLocal()), rulactions(g_rulactions.getLocal()), cacheHitRespRulactions(g_cachehitresprulactions.getLocal()), selfAnsweredRespRulactions(g_selfansweredresprulactions.getLocal()), servers(g_dstates.getLocal()), dynNMGBlock(g_dynblockNMG.getLocal()), dynSMTBlock(g_dynblockSMT.getLocal()), pools(g_pools.getLocal()) { } @@ -815,6 +816,7 @@ struct LocalHolders LocalStateHolder policy; LocalStateHolder > rulactions; LocalStateHolder > cacheHitRespRulactions; + LocalStateHolder > selfAnsweredRespRulactions; LocalStateHolder servers; LocalStateHolder > dynNMGBlock; LocalStateHolder > dynSMTBlock; diff --git a/pdns/dnsdistdist/docs/guides/webserver.rst b/pdns/dnsdistdist/docs/guides/webserver.rst index cdcd5f296..9bfc57216 100644 --- a/pdns/dnsdistdist/docs/guides/webserver.rst +++ b/pdns/dnsdistdist/docs/guides/webserver.rst @@ -103,6 +103,7 @@ URL Endpoints :>json string acl: A string of comma-separated netmasks currently allowed by the :ref:`ACL `. :>json list cache-hit-response-rules: A list of :json:object:`ResponseRule` objects applied on cache hits + :>json list self-answered-response-rules: A list of :json:object:`ResponseRule` objects applied on self-answered queries :>json string daemon_type: The type of daemon, always "dnsdist" :>json list frontends: A list of :json:object:`Frontend` objects :>json list pools: A list of :json:object:`Pool` objects diff --git a/pdns/dnsdistdist/docs/rules-actions.rst b/pdns/dnsdistdist/docs/rules-actions.rst index 9223ed97d..50652a13e 100644 --- a/pdns/dnsdistdist/docs/rules-actions.rst +++ b/pdns/dnsdistdist/docs/rules-actions.rst @@ -365,9 +365,9 @@ For Rules related to responses: Move the last response rule to the first position. -Functions for manipulation Cache Hit Rules: +Functions for manipulating Cache Hit Respone Rules: -.. function:: addCacheHitResponseAction(DNSRule, action) +.. function:: addCacheHitResponseAction(DNSRule, action [, options]) .. versionadded:: 1.2.0 @@ -417,6 +417,49 @@ Functions for manipulation Cache Hit Rules: Move the last cache hit response rule to the first position. +Functions for manipulating Self-Answered Response Rules: + +.. function:: addSelfAnsweredResponseAction(DNSRule, action [, options]) + + .. versionadded:: 1.3.0 + + Add a Rule and Action for Self-Answered queries to the existing rules. + + :param DNSRule: A DNSRule, e.g. an :func:`allRule` or a compounded bunch of rules using e.g. :func:`AndRule` + :param action: The action to take + +.. function:: mvSelfAnsweredResponseRule(from, to) + + .. versionadded:: 1.3.0 + + Move self answered response 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. + + :param int from: Rule number to move + :param int to: Location to more the Rule to + +.. function:: rmSelfAnsweredResponseRule(id) + + .. versionadded:: 1.3.0 + + Remove self answered response rule ``id``. + + :param int id: The UUID of the rule to remove if ``id`` is an UUID, its position otherwise + +.. function:: showSelfAnsweredResponseRules([showUUIDs]) + + .. versionadded:: 1.3.0 + + Show all defined self answered response rules, optionally displaying their UUIDs. + + :param bool showUUIDs: Whether to display the UUIDs, defaults to false + +.. function:: topSelfAnsweredResponseRule() + + .. versionadded:: 1.3.0 + + Move the last self answered response rule to the first position. + .. _RulesIntro: Matching Packets (Selectors) @@ -635,8 +678,8 @@ Combining Rules :param {Rule} selector: A table of Rules -Convience Functions -~~~~~~~~~~~~~~~~~~~ +Convenience Functions +~~~~~~~~~~~~~~~~~~~~~ .. function:: makeRule(rule) diff --git a/regression-tests.dnsdist/test_API.py b/regression-tests.dnsdist/test_API.py index 942dfbed1..caf2ff6ad 100644 --- a/regression-tests.dnsdist/test_API.py +++ b/regression-tests.dnsdist/test_API.py @@ -78,7 +78,7 @@ class TestAPIBasics(DNSDistTest): self.assertEquals(content['daemon_type'], 'dnsdist') - rule_groups = ['response-rules', 'cache-hit-response-rules'] + rule_groups = ['response-rules', 'cache-hit-response-rules', 'self-answered-response-rules'] for key in ['version', 'acl', 'local', 'rules', 'servers', 'frontends', 'pools'] + rule_groups: self.assertIn(key, content) diff --git a/regression-tests.dnsdist/test_SelfAnsweredResponses.py b/regression-tests.dnsdist/test_SelfAnsweredResponses.py new file mode 100644 index 000000000..843e2bd3d --- /dev/null +++ b/regression-tests.dnsdist/test_SelfAnsweredResponses.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python +import dns +from dnsdisttests import DNSDistTest + +class TestSelfAnsweredResponses(DNSDistTest): + + _config_template = """ + -- this is a silly test config, please do not do this in production. + addAction(makeRule("udp.selfanswered.tests.powerdns.com."), SpoofAction("192.0.2.1")) + addSelfAnsweredResponseAction(AndRule({makeRule("udp.selfanswered.tests.powerdns.com."), NotRule(MaxQPSRule(1))}), DropResponseAction()) + addAction(makeRule("tcp.selfanswered.tests.powerdns.com."), SpoofAction("192.0.2.1")) + addSelfAnsweredResponseAction(AndRule({makeRule("tcp.selfanswered.tests.powerdns.com."), NotRule(MaxQPSRule(1))}), DropResponseAction()) + newServer{address="127.0.0.1:%s"} + """ + + def testSelfAnsweredUDP(self): + """ + CacheHitResponse: Drop when served from the cache + """ + ttl = 60 + name = 'udp.selfanswered.tests.powerdns.com.' + query = dns.message.make_query(name, 'A', 'IN') + response = dns.message.make_response(query) + rrset = dns.rrset.from_text(name, + ttl, + dns.rdataclass.IN, + dns.rdatatype.A, + '192.0.2.1') + response.answer.append(rrset) + response.flags |= dns.flags.RA + + # self-answered, but no SelfAnswered rule matches. + (_, receivedResponse) = self.sendUDPQuery(query, response=None, useQueue=False) + self.assertTrue(receivedResponse) + self.assertEquals(receivedResponse, response) + + # self-answered, AND SelfAnswered rule matches. Should not see a reply. + (_, receivedResponse) = self.sendUDPQuery(query, response=None, useQueue=False) + self.assertIsNone(receivedResponse) + + def testSelfAnsweredTCP(self): + """ + testSelfAnsweredTCP: Drop after exceeding QPS + """ + ttl = 60 + name = 'tcp.selfanswered.tests.powerdns.com.' + query = dns.message.make_query(name, 'A', 'IN') + response = dns.message.make_response(query) + rrset = dns.rrset.from_text(name, + ttl, + dns.rdataclass.IN, + dns.rdatatype.A, + '192.0.2.1') + response.answer.append(rrset) + response.flags |= dns.flags.RA + + # self-answered, but no SelfAnswered rule matches. + (_, receivedResponse) = self.sendTCPQuery(query, response=None, useQueue=False) + self.assertTrue(receivedResponse) + self.assertEquals(receivedResponse, response) + + # self-answered, AND SelfAnswered rule matches. Should not see a reply. + (_, receivedResponse) = self.sendTCPQuery(query, response=None, useQueue=False) + self.assertIsNone(receivedResponse)