From e7a1029c067ef9bfc18b4917482773f111af28a8 Mon Sep 17 00:00:00 2001 From: Remi Gacogne Date: Mon, 18 Jan 2016 11:19:40 +0100 Subject: [PATCH] dnsdist: Add NotRule() and OrRule() --- pdns/README-dnsdist.md | 9 +- pdns/dnsdist-lua.cc | 8 + pdns/dnsrulactions.hh | 55 +++++++ regression-tests.dnsdist/test_Advanced.py | 181 ++++++++++++++++++++++ regression-tests.dnsdist/test_Basics.py | 10 +- 5 files changed, 257 insertions(+), 6 deletions(-) diff --git a/pdns/README-dnsdist.md b/pdns/README-dnsdist.md index 40718e13c8..ecf696459b 100644 --- a/pdns/README-dnsdist.md +++ b/pdns/README-dnsdist.md @@ -303,7 +303,10 @@ Rules have selectors and actions. Current selectors are: * Packet requests DNSSEC processing * Query received over UDP or TCP -A special rule is `AndRule{rule1, rule2}`, which only matches if all of its subrules match. +Special rules are: + * `AndRule{rule1, rule2}`, which only matches if all of its subrules match + * `OrRule{rule1, rule2}`, which matches if at least one of its subrules match + * `NotRule(rule)`, which matches if its subrule does not match Current actions are: * Drop (DropAction) @@ -334,6 +337,8 @@ A DNS rule can be: * a MaxQPSIPRule * a MaxQPSRule * a NetmaskGroupRule + * a NotRule + * an OrRule * a QTypeRule * a RegexRule * a SuffixMatchNodeRule @@ -815,6 +820,8 @@ instantiate a server with additional parameters * `MaxQPSIPRule(qps, v4Mask=32, v6Mask=64)`: matches traffic exceeding the qps limit per subnet * `MaxQPSRule(qps)`: matches traffic not exceeding this qps limit * `NetmaskGroupRule()`: matches traffic from the specified network range + * `NotRule()`: matches if the sub-rule does not match + * `OrRule()`: matches if at least one of the sub-rules matches * `QTypeRule(qtype)`: matches queries with the specified qtype * `RegexRule(regex)`: matches the query name against the supplied regex * `SuffixMatchNodeRule()`: matches based on a group of domain suffixes for rapid testing of membership diff --git a/pdns/dnsdist-lua.cc b/pdns/dnsdist-lua.cc index c4793dde31..d05baf19d5 100644 --- a/pdns/dnsdist-lua.cc +++ b/pdns/dnsdist-lua.cc @@ -703,10 +703,18 @@ vector> setupLua(bool client, const std::string& confi return std::shared_ptr(new AndRule(a)); }); + g_lua.writeFunction("OrRule", [](vector > >a) { + return std::shared_ptr(new OrRule(a)); + }); + g_lua.writeFunction("TCPRule", [](bool tcp) { return std::shared_ptr(new TCPRule(tcp)); }); + g_lua.writeFunction("NotRule", [](std::shared_ptrrule) { + return std::shared_ptr(new NotRule(rule)); + }); + g_lua.writeFunction("addAction", [](luadnsrule_t var, std::shared_ptr ea) { setLuaSideEffect(); diff --git a/pdns/dnsrulactions.hh b/pdns/dnsrulactions.hh index 4b6aa5f515..5af45777ff 100644 --- a/pdns/dnsrulactions.hh +++ b/pdns/dnsrulactions.hh @@ -152,6 +152,41 @@ private: }; +class OrRule : public DNSRule +{ +public: + OrRule(const vector > >& rules) + { + for(const auto& r : rules) + d_rules.push_back(r.second); + } + + bool matches(const DNSQuestion* dq) const override + { + auto iter = d_rules.begin(); + for(; iter != d_rules.end(); ++iter) + if((*iter)->matches(dq)) + return true; + return false; + } + + string toString() const override + { + string ret; + for(const auto& rule : d_rules) { + if(!ret.empty()) + ret+= " || "; + ret += "("+ rule->toString()+")"; + } + return ret; + } +private: + + vector > d_rules; + +}; + + class RegexRule : public DNSRule { public: @@ -229,6 +264,26 @@ private: bool d_tcp; }; + +class NotRule : public DNSRule +{ +public: + NotRule(shared_ptr& rule): d_rule(rule) + { + } + bool matches(const DNSQuestion* dq) const override + { + return !d_rule->matches(dq); + } + string toString() const override + { + return "!"+ d_rule->toString(); + } +private: + shared_ptr d_rule; +}; + + class DropAction : public DNSAction { public: diff --git a/regression-tests.dnsdist/test_Advanced.py b/regression-tests.dnsdist/test_Advanced.py index f35a2b0717..e93ea7936f 100644 --- a/regression-tests.dnsdist/test_Advanced.py +++ b/regression-tests.dnsdist/test_Advanced.py @@ -683,3 +683,184 @@ class TestAdvancedLuaSpoof(DNSDistTest): self.assertTrue(receivedResponse) receivedResponse.id = expectedResponse.id self.assertEquals(expectedResponse, receivedResponse) + +class TestAdvancedTruncateAnyAndTCP(DNSDistTest): + + _config_template = """ + truncateTC(false) + addAction(AndRule({QTypeRule("ANY"), TCPRule(true)}), TCAction()) + newServer{address="127.0.0.1:%s"} + """ + def testTruncateAnyOverTCP(self): + """ + Advanced: Truncate ANY over TCP + + Send an ANY query to "anytruncatetcp.tests.powerdns.com.", + should be truncated over TCP, not over UDP (yes, it makes no sense, + deal with it). + """ + name = 'anytruncatetcp.tests.powerdns.com.' + query = dns.message.make_query(name, 'ANY', 'IN') + + response = dns.message.make_response(query) + rrset = dns.rrset.from_text(name, + 3600, + dns.rdataclass.IN, + dns.rdatatype.A, + '127.0.0.1') + + response.answer.append(rrset) + + (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response) + self.assertTrue(receivedQuery) + self.assertTrue(receivedResponse) + receivedQuery.id = query.id + receivedResponse.id = response.id + self.assertEquals(query, receivedQuery) + self.assertEquals(receivedResponse, response) + + expectedResponse = dns.message.make_response(query) + expectedResponse.flags |= dns.flags.TC + + (_, receivedResponse) = self.sendTCPQuery(query, response=None, useQueue=False) + receivedResponse.id = expectedResponse.id + self.assertEquals(receivedResponse, expectedResponse) + +class TestAdvancedAndNot(DNSDistTest): + + _config_template = """ + addAction(AndRule({NotRule(QTypeRule("A")), TCPRule(false)}), RCodeAction(4)) + newServer{address="127.0.0.1:%s"} + """ + def testAOverUDPReturnsNotImplementedCanary(self): + """ + Advanced: !A && UDP canary + + dnsdist is configured to reply 'not implemented' for query + over UDP AND !qtype A. + We send an A query over UDP and TCP, and check that the + response is OK. + """ + name = 'andnot.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.A, + '127.0.0.1') + response.answer.append(rrset) + + (receivedQuery, receivedResponse) = self.sendUDPQuery(query, response) + self.assertTrue(receivedQuery) + self.assertTrue(receivedResponse) + receivedQuery.id = query.id + receivedResponse.id = response.id + self.assertEquals(query, receivedQuery) + self.assertEquals(receivedResponse, response) + + (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response) + self.assertTrue(receivedQuery) + self.assertTrue(receivedResponse) + receivedQuery.id = query.id + receivedResponse.id = response.id + self.assertEquals(query, receivedQuery) + self.assertEquals(receivedResponse, response) + + def testAOverUDPReturnsNotImplemented(self): + """ + Advanced: !A && UDP + + dnsdist is configured to reply 'not implemented' for query + over UDP AND !qtype A. + We send a TXT query over UDP and TCP, and check that the + response is OK for TCP and 'not implemented' for UDP. + """ + name = 'andnot.tests.powerdns.com.' + query = dns.message.make_query(name, 'TXT', 'IN') + + expectedResponse = dns.message.make_response(query) + expectedResponse.set_rcode(dns.rcode.NOTIMP) + + (_, receivedResponse) = self.sendUDPQuery(query, response=None, useQueue=False) + receivedResponse.id = expectedResponse.id + self.assertEquals(receivedResponse, expectedResponse) + + response = dns.message.make_response(query) + rrset = dns.rrset.from_text(name, + 3600, + dns.rdataclass.IN, + dns.rdatatype.TXT, + 'nothing to see here') + response.answer.append(rrset) + + (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response) + self.assertTrue(receivedQuery) + self.assertTrue(receivedResponse) + receivedQuery.id = query.id + receivedResponse.id = response.id + self.assertEquals(query, receivedQuery) + self.assertEquals(receivedResponse, response) + +class TestAdvancedOr(DNSDistTest): + + _config_template = """ + addAction(OrRule({QTypeRule("A"), TCPRule(false)}), RCodeAction(4)) + newServer{address="127.0.0.1:%s"} + """ + def testAAAAOverUDPReturnsNotImplemented(self): + """ + Advanced: A || UDP: AAAA + + dnsdist is configured to reply 'not implemented' for query + over UDP OR qtype A. + We send an AAAA query over UDP and TCP, and check that the + response is 'not implemented' for UDP and OK for TCP. + """ + name = 'aorudp.tests.powerdns.com.' + query = dns.message.make_query(name, 'AAAA', '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) + + expectedResponse = dns.message.make_response(query) + expectedResponse.set_rcode(dns.rcode.NOTIMP) + + (_, receivedResponse) = self.sendUDPQuery(query, response=None, useQueue=False) + receivedResponse.id = expectedResponse.id + self.assertEquals(receivedResponse, expectedResponse) + + (receivedQuery, receivedResponse) = self.sendTCPQuery(query, response) + self.assertTrue(receivedQuery) + self.assertTrue(receivedResponse) + receivedQuery.id = query.id + receivedResponse.id = response.id + self.assertEquals(query, receivedQuery) + self.assertEquals(receivedResponse, response) + + def testAOverUDPReturnsNotImplemented(self): + """ + Advanced: A || UDP: A + + dnsdist is configured to reply 'not implemented' for query + over UDP OR qtype A. + We send an A query over UDP and TCP, and check that the + response is 'not implemented' for both. + """ + name = 'aorudp.tests.powerdns.com.' + query = dns.message.make_query(name, 'A', 'IN') + + expectedResponse = dns.message.make_response(query) + expectedResponse.set_rcode(dns.rcode.NOTIMP) + + (_, receivedResponse) = self.sendUDPQuery(query, response=None, useQueue=False) + receivedResponse.id = expectedResponse.id + self.assertEquals(receivedResponse, expectedResponse) + + (_, receivedResponse) = self.sendTCPQuery(query, response=None, useQueue=False) + receivedResponse.id = expectedResponse.id + self.assertEquals(receivedResponse, expectedResponse) diff --git a/regression-tests.dnsdist/test_Basics.py b/regression-tests.dnsdist/test_Basics.py index 4414481bb1..68baf8c28e 100644 --- a/regression-tests.dnsdist/test_Basics.py +++ b/regression-tests.dnsdist/test_Basics.py @@ -57,7 +57,7 @@ class TestBasics(DNSDistTest): name = 'simplea.tests.powerdns.com.' query = dns.message.make_query(name, 'A', 'IN', use_edns=False) response = dns.message.make_response(query) - rrset = dns.rrset.from_text('simplea.tests.powerdns.com.', + rrset = dns.rrset.from_text(name, 3600, dns.rdataclass.IN, dns.rdatatype.A, @@ -98,7 +98,7 @@ class TestBasics(DNSDistTest): self.assertEquals(receivedResponse, expectedResponse) response = dns.message.make_response(query) - rrset = dns.rrset.from_text('any.tests.powerdns.com.', + rrset = dns.rrset.from_text(name, 3600, dns.rdataclass.IN, dns.rdatatype.A, @@ -173,7 +173,7 @@ class TestBasics(DNSDistTest): Basics: NOTIMPL for specific name and qtype dnsdist is configured to reply 'not implemented' for query - matching "nameAndQtype.tests.powerdns.com." AND qtype TXT/ + matching "nameAndQtype.tests.powerdns.com." AND qtype TXT. We send a TXT query for "nameAndQtype.powerdns.com." and check that the response is 'not implemented'. """ @@ -195,7 +195,7 @@ class TestBasics(DNSDistTest): Basics: NOTIMPL qtype canary dnsdist is configured to reply 'not implemented' for query - matching "nameAndQtype.tests.powerdns.com." AND qtype TXT/ + matching "nameAndQtype.tests.powerdns.com." AND qtype TXT. We send a A query for "nameAndQtype.tests.powerdns.com." and check that the response is OK. """ @@ -230,7 +230,7 @@ class TestBasics(DNSDistTest): Basics: NOTIMPL qname canary dnsdist is configured to reply 'not implemented' for query - matching "nameAndQtype.tests.powerdns.com." AND qtype TXT/ + matching "nameAndQtype.tests.powerdns.com." AND qtype TXT. We send a TXT query for "OtherNameAndQtype.tests.powerdns.com." and check that the response is OK. """ -- 2.47.2